alex-c-line 2.10.2 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,7 @@ declare let scripts: {
65
65
  command: string;
66
66
  "create-local-package": string;
67
67
  "create-release-note": string;
68
+ dev: string;
68
69
  format: string;
69
70
  "format-eslint": string;
70
71
  "format-markdownlint": string;
@@ -65,6 +65,7 @@ declare let scripts: {
65
65
  command: string;
66
66
  "create-local-package": string;
67
67
  "create-release-note": string;
68
+ dev: string;
68
69
  format: string;
69
70
  "format-eslint": string;
70
71
  "format-markdownlint": string;
package/dist/index.cjs CHANGED
@@ -303,13 +303,33 @@ async function findPackageRoot(startDirectory, packageName) {
303
303
  const __filename$3 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
304
304
  const ALEX_C_LINE_PACKAGE_ROOT = findPackageRoot(node_path.default.dirname(__filename$3), "alex-c-line");
305
305
  //#endregion
306
+ //#region src/utility/licenses/resolveLicense.ts
307
+ function resolveLicense(license) {
308
+ return license.replaceAll(/[()]/g, "").split(/\s+OR\s+/).map((part) => {
309
+ return part.trim();
310
+ });
311
+ }
312
+ //#endregion
313
+ //#region src/utility/licenses/isValidLicense.ts
314
+ function isValidLicense(license, allowedLicenses) {
315
+ return resolveLicense(license).some((license) => {
316
+ return allowedLicenses.includes(license);
317
+ });
318
+ }
319
+ //#endregion
306
320
  //#region src/cli/commands/internal/dependency-audit/helpers/getLicenseCheck.ts
307
321
  const ALLOWED_LICENSES = [
308
322
  "MIT",
323
+ "MIT-0",
309
324
  "ISC",
310
325
  "Apache-2.0",
311
326
  "BSD-2-Clause",
312
- "BSD-3-Clause"
327
+ "BSD-3-Clause",
328
+ "BlueOak-1.0.0",
329
+ "Python-2.0",
330
+ "0BSD",
331
+ "Unlicense",
332
+ "CC0-1.0"
313
333
  ];
314
334
  const pnpmLicensesSchema = zod.default.record(zod.default.string(), zod.default.array(zod.default.object({
315
335
  name: zod.default.string(),
@@ -336,7 +356,7 @@ async function getLicenseCheck(program) {
336
356
  return summaryTableRowTemplate.replace("{{license}}", (0, _alextheman_utility.escapeHTML)(license)).replace("{{count}}", (0, _alextheman_utility.escapeHTML)(data.length.toString()));
337
357
  }).join("\n"));
338
358
  const invalidLicenses = licenseEntries.filter(([license, _]) => {
339
- return !ALLOWED_LICENSES.includes(license);
359
+ return !isValidLicense(license, ALLOWED_LICENSES);
340
360
  });
341
361
  let invalidSummary;
342
362
  if (invalidLicenses.length === 0) invalidSummary = "No licenses requiring review.";
@@ -581,7 +601,7 @@ async function readdirSafe(path) {
581
601
  //#region src/cli/commands/internal/media/generate.ts
582
602
  function internalMediaGenerate(program) {
583
603
  program.command("generate").argument("[target]", "The directory to generate from.", process.cwd()).option("--ignore <ignore>", "Extra directories to ignore as comma-separated list.").option("-r --resolution <resolution>", "The resolution of the rendered scenes, with a comma between width and height.").action(async (target, { ignore, resolution }) => {
584
- const ignored = new Set([
604
+ const ignored = /* @__PURE__ */ new Set([
585
605
  ".git",
586
606
  "node_modules",
587
607
  "__pycache__",
@@ -1081,6 +1101,58 @@ function root(program) {
1081
1101
  });
1082
1102
  }
1083
1103
  //#endregion
1104
+ //#region src/cli/commands/template/changeRequest/helpers/resolveBasename.ts
1105
+ function resolveBasename(basename) {
1106
+ return basename.trim().replace(/[.!?/]+$/, "").toLowerCase().replace(/\s+/g, "_");
1107
+ }
1108
+ //#endregion
1109
+ //#region src/utility/fileSystem/getNextFileName.ts
1110
+ function getNextFileName(directoryContents, newFileName) {
1111
+ const extension = node_path.default.extname(newFileName);
1112
+ const baseName = node_path.default.basename(newFileName, extension);
1113
+ const fileRegex = new RegExp(String.raw`^${(0, _alextheman_utility.escapeRegexPattern)(baseName)}(?:_(\d+))?${(0, _alextheman_utility.escapeRegexPattern)(extension)}$`);
1114
+ const matches = directoryContents.map((fileName) => {
1115
+ return fileName.match(fileRegex);
1116
+ }).filter((match) => {
1117
+ return match !== null;
1118
+ });
1119
+ if (matches.length === 0) return newFileName;
1120
+ const highestSuffix = Math.max(...matches.map((match) => {
1121
+ return match[1] === void 0 ? 0 : (0, _alextheman_utility.parseIntStrict)(match[1]);
1122
+ }));
1123
+ return highestSuffix === 0 ? `${baseName}_1${extension}` : `${baseName}_${highestSuffix + 1}${extension}`;
1124
+ }
1125
+ //#endregion
1126
+ //#region src/utility/markdownTemplates/replaceMarkdownPlaceholders.ts
1127
+ function replaceMarkdownPlaceholders(rawContent, templateVariables) {
1128
+ const { content, data } = (0, gray_matter.default)(rawContent);
1129
+ const placeholders = _alextheman_utility.az.with(zod.default.array(zod.default.string()).default([])).parse(data.placeholders);
1130
+ let finalContent = content;
1131
+ for (const placeholder of placeholders) {
1132
+ if (!(placeholder in templateVariables)) throw new _alextheman_utility_v6.DataError({ placeholder }, "INVALID_PLACEHOLDER", "The placeholder found in frontmatter can not be found in the metadata.");
1133
+ finalContent = finalContent.replaceAll(`{{${placeholder}}}`, templateVariables[placeholder]);
1134
+ }
1135
+ return finalContent;
1136
+ }
1137
+ //#endregion
1138
+ //#region src/cli/commands/template/changeRequest/create.ts
1139
+ function templateChangeRequestCreate(program) {
1140
+ program.command("create").description("Generate a change request document.").requiredOption("-p, --project-name <projectName>", "The project to request a change for.").requiredOption("-r, --reason <reason>", "The reason for the change.").option("-d, --description <description>", "A further description of the change.", "Redeploy the service.").requiredOption("-q, --requested-by <requestedBy>", "The user making the change request.").option("-o, --output-directory <outputDirectory>", "The directory to output the document in. The name of the document is determined by the reason.", node_path.default.join("docs", "changeRequests")).action(async ({ outputDirectory, ...templateVariables }) => {
1141
+ const resolvedOutputDirectory = node_path.default.resolve(outputDirectory);
1142
+ const newDocumentContents = replaceMarkdownPlaceholders(await (0, node_fs_promises.readFile)(node_path.default.join(await ALEX_C_LINE_PACKAGE_ROOT, "templates", "changeRequest", "template.md"), "utf-8"), templateVariables);
1143
+ await (0, node_fs_promises.mkdir)(resolvedOutputDirectory, { recursive: true });
1144
+ const newFileName = getNextFileName(await (0, node_fs_promises.readdir)(resolvedOutputDirectory), `${resolveBasename(templateVariables.reason)}.md`);
1145
+ const outputPath = node_path.default.join(resolvedOutputDirectory, newFileName);
1146
+ await (0, node_fs_promises.writeFile)(outputPath, newDocumentContents);
1147
+ console.info(`Change document successfully written to \`${outputPath}\`.`);
1148
+ });
1149
+ }
1150
+ //#endregion
1151
+ //#region src/cli/commands/template/changeRequest/index.ts
1152
+ function templateChangeRequest(program) {
1153
+ loadCommands(program.command("change-request").description("Manage change request documents."), { templateChangeRequestCreate });
1154
+ }
1155
+ //#endregion
1084
1156
  //#region src/utility/markdownTemplates/pullRequest/createPullRequestTemplatesFromTemplates.ts
1085
1157
  const __filename$2 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
1086
1158
  function getTemplateVariables$1(config) {
@@ -1277,18 +1349,6 @@ async function getEditableSectionFromTemplate(version) {
1277
1349
  return await (0, node_fs_promises.readFile)(node_path.default.join(templatesPath, version.type === "major" ? "editableSectionMajor.md" : "editableSection.md"), "utf-8");
1278
1350
  }
1279
1351
  //#endregion
1280
- //#region src/utility/markdownTemplates/replaceMarkdownPlaceholders.ts
1281
- function replaceMarkdownPlaceholders(rawContent, templateVariables) {
1282
- const { content, data } = (0, gray_matter.default)(rawContent);
1283
- const placeholders = _alextheman_utility.az.with(zod.default.array(zod.default.string()).default([])).parse(data.placeholders);
1284
- let finalContent = content;
1285
- for (const placeholder of placeholders) {
1286
- if (!(placeholder in templateVariables)) throw new _alextheman_utility_v6.DataError({ placeholder }, "INVALID_PLACEHOLDER", "The placeholder found in frontmatter can not be found in the metadata.");
1287
- finalContent = finalContent.replaceAll(`{{${placeholder}}}`, templateVariables[placeholder]);
1288
- }
1289
- return finalContent;
1290
- }
1291
- //#endregion
1292
1352
  //#region src/utility/markdownTemplates/releaseNote/createReleaseNoteFromTemplates.ts
1293
1353
  async function getTemplateVariables(projectName, version, templateVariables) {
1294
1354
  if ("editableSection" in templateVariables && templateVariables.editableSection) {
@@ -1416,6 +1476,7 @@ function templateReleaseNote(program) {
1416
1476
  //#region src/cli/commands/template/index.ts
1417
1477
  function template(program) {
1418
1478
  loadCommands(program.command("template").description("Manage the automatically-generatable template files."), {
1479
+ templateChangeRequest,
1419
1480
  templatePullRequest,
1420
1481
  templateReleaseNote
1421
1482
  });
@@ -1423,7 +1484,7 @@ function template(program) {
1423
1484
  //#endregion
1424
1485
  //#region package.json
1425
1486
  var name = "alex-c-line";
1426
- var version$1 = "2.10.2";
1487
+ var version$1 = "2.11.0";
1427
1488
  var description = "Command-line tool with commands to streamline the developer workflow.";
1428
1489
  //#endregion
1429
1490
  //#region src/utility/updates/checkUpdate.ts
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import boxen from "boxen";
7
7
  import figlet from "figlet";
8
8
  import envPaths from "env-paths";
9
9
  import path from "node:path";
10
- import { ONE_DAY_IN_MILLISECONDS, VersionNumber, az, escapeHTML, fillArray, getStringsAndInterpolations, interpolate, isTemplateStringsArray, kebabToCamel, normaliseIndents, omitProperties, parseBoolean, parseVersionType, removeDuplicates, removeUndefinedFromObject, sortBy, stringifyDotenv } from "@alextheman/utility";
10
+ import { ONE_DAY_IN_MILLISECONDS, VersionNumber, az, escapeHTML, escapeRegexPattern, fillArray, getStringsAndInterpolations, interpolate, isTemplateStringsArray, kebabToCamel, normaliseIndents, omitProperties, parseBoolean, parseIntStrict, parseVersionType, removeDuplicates, removeUndefinedFromObject, sortBy, stringifyDotenv } from "@alextheman/utility";
11
11
  import { confirm, input, password, select } from "@inquirer/prompts";
12
12
  import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
13
13
  import { parse } from "dotenv";
@@ -272,13 +272,33 @@ async function findPackageRoot(startDirectory, packageName) {
272
272
  const __filename$2 = fileURLToPath(import.meta.url);
273
273
  const ALEX_C_LINE_PACKAGE_ROOT = findPackageRoot(path.dirname(__filename$2), "alex-c-line");
274
274
  //#endregion
275
+ //#region src/utility/licenses/resolveLicense.ts
276
+ function resolveLicense(license) {
277
+ return license.replaceAll(/[()]/g, "").split(/\s+OR\s+/).map((part) => {
278
+ return part.trim();
279
+ });
280
+ }
281
+ //#endregion
282
+ //#region src/utility/licenses/isValidLicense.ts
283
+ function isValidLicense(license, allowedLicenses) {
284
+ return resolveLicense(license).some((license) => {
285
+ return allowedLicenses.includes(license);
286
+ });
287
+ }
288
+ //#endregion
275
289
  //#region src/cli/commands/internal/dependency-audit/helpers/getLicenseCheck.ts
276
290
  const ALLOWED_LICENSES = [
277
291
  "MIT",
292
+ "MIT-0",
278
293
  "ISC",
279
294
  "Apache-2.0",
280
295
  "BSD-2-Clause",
281
- "BSD-3-Clause"
296
+ "BSD-3-Clause",
297
+ "BlueOak-1.0.0",
298
+ "Python-2.0",
299
+ "0BSD",
300
+ "Unlicense",
301
+ "CC0-1.0"
282
302
  ];
283
303
  const pnpmLicensesSchema = z.record(z.string(), z.array(z.object({
284
304
  name: z.string(),
@@ -305,7 +325,7 @@ async function getLicenseCheck(program) {
305
325
  return summaryTableRowTemplate.replace("{{license}}", escapeHTML(license)).replace("{{count}}", escapeHTML(data.length.toString()));
306
326
  }).join("\n"));
307
327
  const invalidLicenses = licenseEntries.filter(([license, _]) => {
308
- return !ALLOWED_LICENSES.includes(license);
328
+ return !isValidLicense(license, ALLOWED_LICENSES);
309
329
  });
310
330
  let invalidSummary;
311
331
  if (invalidLicenses.length === 0) invalidSummary = "No licenses requiring review.";
@@ -550,7 +570,7 @@ async function readdirSafe(path) {
550
570
  //#region src/cli/commands/internal/media/generate.ts
551
571
  function internalMediaGenerate(program) {
552
572
  program.command("generate").argument("[target]", "The directory to generate from.", process.cwd()).option("--ignore <ignore>", "Extra directories to ignore as comma-separated list.").option("-r --resolution <resolution>", "The resolution of the rendered scenes, with a comma between width and height.").action(async (target, { ignore, resolution }) => {
553
- const ignored = new Set([
573
+ const ignored = /* @__PURE__ */ new Set([
554
574
  ".git",
555
575
  "node_modules",
556
576
  "__pycache__",
@@ -1050,6 +1070,58 @@ function root(program) {
1050
1070
  });
1051
1071
  }
1052
1072
  //#endregion
1073
+ //#region src/cli/commands/template/changeRequest/helpers/resolveBasename.ts
1074
+ function resolveBasename(basename) {
1075
+ return basename.trim().replace(/[.!?/]+$/, "").toLowerCase().replace(/\s+/g, "_");
1076
+ }
1077
+ //#endregion
1078
+ //#region src/utility/fileSystem/getNextFileName.ts
1079
+ function getNextFileName(directoryContents, newFileName) {
1080
+ const extension = path.extname(newFileName);
1081
+ const baseName = path.basename(newFileName, extension);
1082
+ const fileRegex = new RegExp(String.raw`^${escapeRegexPattern(baseName)}(?:_(\d+))?${escapeRegexPattern(extension)}$`);
1083
+ const matches = directoryContents.map((fileName) => {
1084
+ return fileName.match(fileRegex);
1085
+ }).filter((match) => {
1086
+ return match !== null;
1087
+ });
1088
+ if (matches.length === 0) return newFileName;
1089
+ const highestSuffix = Math.max(...matches.map((match) => {
1090
+ return match[1] === void 0 ? 0 : parseIntStrict(match[1]);
1091
+ }));
1092
+ return highestSuffix === 0 ? `${baseName}_1${extension}` : `${baseName}_${highestSuffix + 1}${extension}`;
1093
+ }
1094
+ //#endregion
1095
+ //#region src/utility/markdownTemplates/replaceMarkdownPlaceholders.ts
1096
+ function replaceMarkdownPlaceholders(rawContent, templateVariables) {
1097
+ const { content, data } = matter(rawContent);
1098
+ const placeholders = az.with(z.array(z.string()).default([])).parse(data.placeholders);
1099
+ let finalContent = content;
1100
+ for (const placeholder of placeholders) {
1101
+ if (!(placeholder in templateVariables)) throw new DataError({ placeholder }, "INVALID_PLACEHOLDER", "The placeholder found in frontmatter can not be found in the metadata.");
1102
+ finalContent = finalContent.replaceAll(`{{${placeholder}}}`, templateVariables[placeholder]);
1103
+ }
1104
+ return finalContent;
1105
+ }
1106
+ //#endregion
1107
+ //#region src/cli/commands/template/changeRequest/create.ts
1108
+ function templateChangeRequestCreate(program) {
1109
+ program.command("create").description("Generate a change request document.").requiredOption("-p, --project-name <projectName>", "The project to request a change for.").requiredOption("-r, --reason <reason>", "The reason for the change.").option("-d, --description <description>", "A further description of the change.", "Redeploy the service.").requiredOption("-q, --requested-by <requestedBy>", "The user making the change request.").option("-o, --output-directory <outputDirectory>", "The directory to output the document in. The name of the document is determined by the reason.", path.join("docs", "changeRequests")).action(async ({ outputDirectory, ...templateVariables }) => {
1110
+ const resolvedOutputDirectory = path.resolve(outputDirectory);
1111
+ const newDocumentContents = replaceMarkdownPlaceholders(await readFile(path.join(await ALEX_C_LINE_PACKAGE_ROOT, "templates", "changeRequest", "template.md"), "utf-8"), templateVariables);
1112
+ await mkdir(resolvedOutputDirectory, { recursive: true });
1113
+ const newFileName = getNextFileName(await readdir(resolvedOutputDirectory), `${resolveBasename(templateVariables.reason)}.md`);
1114
+ const outputPath = path.join(resolvedOutputDirectory, newFileName);
1115
+ await writeFile(outputPath, newDocumentContents);
1116
+ console.info(`Change document successfully written to \`${outputPath}\`.`);
1117
+ });
1118
+ }
1119
+ //#endregion
1120
+ //#region src/cli/commands/template/changeRequest/index.ts
1121
+ function templateChangeRequest(program) {
1122
+ loadCommands(program.command("change-request").description("Manage change request documents."), { templateChangeRequestCreate });
1123
+ }
1124
+ //#endregion
1053
1125
  //#region src/utility/markdownTemplates/pullRequest/createPullRequestTemplatesFromTemplates.ts
1054
1126
  const __filename$1 = fileURLToPath(import.meta.url);
1055
1127
  function getTemplateVariables$1(config) {
@@ -1246,18 +1318,6 @@ async function getEditableSectionFromTemplate(version) {
1246
1318
  return await readFile(path.join(templatesPath, version.type === "major" ? "editableSectionMajor.md" : "editableSection.md"), "utf-8");
1247
1319
  }
1248
1320
  //#endregion
1249
- //#region src/utility/markdownTemplates/replaceMarkdownPlaceholders.ts
1250
- function replaceMarkdownPlaceholders(rawContent, templateVariables) {
1251
- const { content, data } = matter(rawContent);
1252
- const placeholders = az.with(z.array(z.string()).default([])).parse(data.placeholders);
1253
- let finalContent = content;
1254
- for (const placeholder of placeholders) {
1255
- if (!(placeholder in templateVariables)) throw new DataError({ placeholder }, "INVALID_PLACEHOLDER", "The placeholder found in frontmatter can not be found in the metadata.");
1256
- finalContent = finalContent.replaceAll(`{{${placeholder}}}`, templateVariables[placeholder]);
1257
- }
1258
- return finalContent;
1259
- }
1260
- //#endregion
1261
1321
  //#region src/utility/markdownTemplates/releaseNote/createReleaseNoteFromTemplates.ts
1262
1322
  async function getTemplateVariables(projectName, version, templateVariables) {
1263
1323
  if ("editableSection" in templateVariables && templateVariables.editableSection) {
@@ -1385,6 +1445,7 @@ function templateReleaseNote(program) {
1385
1445
  //#region src/cli/commands/template/index.ts
1386
1446
  function template(program) {
1387
1447
  loadCommands(program.command("template").description("Manage the automatically-generatable template files."), {
1448
+ templateChangeRequest,
1388
1449
  templatePullRequest,
1389
1450
  templateReleaseNote
1390
1451
  });
@@ -1392,7 +1453,7 @@ function template(program) {
1392
1453
  //#endregion
1393
1454
  //#region package.json
1394
1455
  var name = "alex-c-line";
1395
- var version$1 = "2.10.2";
1456
+ var version$1 = "2.11.0";
1396
1457
  var description = "Command-line tool with commands to streamline the developer workflow.";
1397
1458
  //#endregion
1398
1459
  //#region src/utility/updates/checkUpdate.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alex-c-line",
3
- "version": "2.10.2",
3
+ "version": "2.11.0",
4
4
  "description": "Command-line tool with commands to streamline the developer workflow.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,9 +34,9 @@
34
34
  "templates"
35
35
  ],
36
36
  "dependencies": {
37
- "@alextheman/utility": "5.22.0",
37
+ "@alextheman/utility": "5.23.0",
38
38
  "@inquirer/prompts": "8.5.2",
39
- "axios": "1.18.0",
39
+ "axios": "1.18.1",
40
40
  "boxen": "8.0.1",
41
41
  "chalk": "5.6.2",
42
42
  "commander": "15.0.0",
@@ -46,16 +46,17 @@
46
46
  "execa": "9.6.1",
47
47
  "figlet": "1.11.0",
48
48
  "gray-matter": "4.0.3",
49
- "semver": "7.8.4",
49
+ "semver": "7.8.5",
50
50
  "supports-color": "10.2.2",
51
51
  "toml": "4.1.1",
52
52
  "zod": "4.4.3"
53
53
  },
54
54
  "devDependencies": {
55
- "@alextheman/eslint-plugin": "5.17.1",
55
+ "@alextheman/eslint-plugin": "5.18.0",
56
56
  "@commander-js/extra-typings": "15.0.0",
57
+ "@faker-js/faker": "10.5.0",
57
58
  "@types/eslint": "9.6.1",
58
- "@types/node": "25.9.3",
59
+ "@types/node": "26.0.1",
59
60
  "@types/semver": "7.7.1",
60
61
  "@types/update-notifier": "6.0.8",
61
62
  "cross-env": "10.1.0",
@@ -66,10 +67,11 @@
66
67
  "prettier": "3.8.4",
67
68
  "tempy": "3.2.0",
68
69
  "ts-node": "10.9.2",
69
- "tsdown": "0.22.2",
70
+ "tsdown": "0.22.3",
71
+ "tsx": "4.22.4",
70
72
  "typescript": "6.0.3",
71
- "typescript-eslint": "8.61.1",
72
- "vite": "8.0.16",
73
+ "typescript-eslint": "8.62.0",
74
+ "vite": "8.1.0",
73
75
  "vitest": "4.1.9"
74
76
  },
75
77
  "engines": {
@@ -80,6 +82,7 @@
80
82
  "command": "bash -c 'pnpm run build && echo && echo \"Command output:\" && node dist/index.js \"$@\"' --",
81
83
  "create-local-package": "pnpm run build && rm -f alex-c-line-*.tgz && pnpm pack",
82
84
  "create-release-note": "bash -c 'git pull origin main && pnpm run command template release-note create $@' --",
85
+ "dev": "tsx src/cli/index.ts",
83
86
  "format": "pnpm run format-prettier && pnpm run format-eslint && pnpm run format-markdownlint",
84
87
  "format-eslint": "eslint --fix --suppress-all \"package.json\" \"{src,tests}/**/*.ts\" && rm -f eslint-suppressions.json",
85
88
  "format-markdownlint": "pnpm exec markdownlint-cli2 --fix \"**/*.md\" \"!{node_modules,dist}/**\" | grep -v \"^Finding:\"",
@@ -0,0 +1,18 @@
1
+ ---
2
+ placeholders:
3
+ - projectName
4
+ - requestedBy
5
+ - reason
6
+ - description
7
+ ---
8
+ # Change Request
9
+
10
+ This is a request for a change to {{projectName}} by {{requestedBy}}. If this gets accepted it will trigger a new deployment to apply the change. Please see more details below
11
+
12
+ ## Change reason
13
+
14
+ {{reason}}
15
+
16
+ ## Change description
17
+
18
+ {{description}}