github-action-readme-generator 1.12.8 → 2.0.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.
@@ -2,6 +2,5 @@
2
2
  /**
3
3
  * Creates a ReadmeGenerator instance and generates docs.
4
4
  */
5
- declare function generateReadme(): Promise<void>;
6
- //#endregion
7
- export { generateReadme };
5
+ export declare function generateReadme(): Promise<void>;
6
+ //#endregion
package/dist/mjs/index.js CHANGED
@@ -10,7 +10,6 @@ import * as feather from "feather-icons";
10
10
  import { icons } from "feather-icons";
11
11
  import chalkPkg from "chalk";
12
12
  import { execFileSync, execSync } from "node:child_process";
13
- import { EOL } from "node:os";
14
13
  import * as markdown from "prettier/plugins/markdown";
15
14
  import * as yaml from "prettier/plugins/yaml";
16
15
  import { format } from "prettier/standalone";
@@ -717,14 +716,15 @@ function getCurrentVersionString(inputs) {
717
716
  const actionDir = path$1.dirname(inputs.action.path);
718
717
  log.debug(`version source: ${versionSource}`);
719
718
  let detectedVersion;
720
- if (versionSource === "explicit") if (override && override.length > 0) {
721
- detectedVersion = override;
722
- log.debug(`using explicit version override: ${detectedVersion}`);
719
+ if (versionSource === "explicit") {
720
+ if (override && override.length > 0) {
721
+ detectedVersion = override;
722
+ log.debug(`using explicit version override: ${detectedVersion}`);
723
+ } else {
724
+ log.debug("explicit mode but no version_override set, falling back to 0.0.0");
725
+ detectedVersion = "0.0.0";
726
+ }
723
727
  } else {
724
- log.debug("explicit mode but no version_override set, falling back to 0.0.0");
725
- detectedVersion = "0.0.0";
726
- }
727
- else {
728
728
  switch (versionSource) {
729
729
  case "git-branch":
730
730
  detectedVersion = getVersionFromGitBranch(actionDir, log);
@@ -859,14 +859,51 @@ const startTokenFormat = "(^|[^`\\\\])<!--\\s+start\\s+%s\\s+-->";
859
859
  * The format for the end token of a section.
860
860
  */
861
861
  const endTokenFormat = "(^|[^`\\\\])<!--\\s+end\\s+%s\\s+-->";
862
+ /**
863
+ * Lays out section content the way it sits between its markers.
864
+ * @param {string} content - The trimmed section content.
865
+ * @param {boolean} addNewlines - Whether to pad the content with newlines.
866
+ * @returns {string} - The text that goes between the markers.
867
+ */
868
+ function layoutSpan(content, addNewlines) {
869
+ return addNewlines ? `\n\n${content}\n` : content;
870
+ }
871
+ /**
872
+ * True when every line break in `text` is CRLF, and there is at least one.
873
+ *
874
+ * Only a document that is CRLF throughout is edited as CRLF. Removing the `\r`
875
+ * before each `\n` and adding it back is then an exact round trip, so the
876
+ * bytes outside the markers survive. A document that mixes the two is left as
877
+ * it is, since no single ending would reproduce it.
878
+ * @param {string} text - The document.
879
+ * @returns {boolean} - Whether the document uses CRLF line endings.
880
+ */
881
+ function usesCrlf(text) {
882
+ return text.includes("\r\n") && !/(^|[^\r])\n/.test(text);
883
+ }
862
884
  var ReadmeEditor = class {
863
885
  log = new LogTask("ReadmeEditor");
864
886
  /**
865
887
  * The path to the README file.
866
888
  */
867
889
  filePath;
890
+ /**
891
+ * The document with LF line endings, whatever the file uses. Every edit and
892
+ * every formatter pass works on LF; `dumpToFile` restores the file's own
893
+ * ending on the way out.
894
+ */
868
895
  fileContent;
869
896
  /**
897
+ * Whether the file on disk is CRLF throughout — see `usesCrlf`.
898
+ */
899
+ crlf = false;
900
+ /**
901
+ * The padded sections this editor has replaced, each against the content it
902
+ * wrote. `dumpToFile` formats these spans and nothing else — see
903
+ * `formatUpdatedSections`.
904
+ */
905
+ updatedSections = /* @__PURE__ */ new Map();
906
+ /**
870
907
  * Creates a new instance of `ReadmeEditor`.
871
908
  * @param {string} filePath - The path to the README file.
872
909
  */
@@ -874,15 +911,17 @@ var ReadmeEditor = class {
874
911
  this.filePath = filePath;
875
912
  try {
876
913
  fs.accessSync(filePath);
877
- this.fileContent = fs.readFileSync(filePath, "utf8");
878
- if (process.env.GITHUB_ACTIONS) core.setOutput("readme_before", this.fileContent);
914
+ const raw = fs.readFileSync(filePath, "utf8");
915
+ if (process.env.GITHUB_ACTIONS) core.setOutput("readme_before", raw);
916
+ this.crlf = usesCrlf(raw);
917
+ this.fileContent = this.crlf ? raw.replaceAll("\r\n", "\n") : raw;
879
918
  } catch (error) {
880
919
  this.log.fail(`Readme at '${filePath}' does not exist.`);
881
920
  throw error;
882
921
  }
883
922
  }
884
923
  /**
885
- * Gets the current README content.
924
+ * Gets the current README content, with LF line endings.
886
925
  * @returns {string} - The README file content.
887
926
  */
888
927
  getReadmeContent() {
@@ -917,24 +956,66 @@ var ReadmeEditor = class {
917
956
  */
918
957
  updateSection(name, providedContent, addNewlines = true) {
919
958
  const log = new LogTask(name);
920
- const content = (Array.isArray(providedContent) ? providedContent.join(EOL) : providedContent ?? "").trim();
959
+ const content = (Array.isArray(providedContent) ? providedContent.join("\n") : providedContent ?? "").replaceAll("\r\n", "\n").trim();
921
960
  log.info(`Looking for the ${name} token in ${this.filePath}`);
922
961
  const [startIndex, stopIndex] = this.getTokenIndexes(name, log);
923
962
  if (startIndex && stopIndex) {
924
963
  const beforeContent = this.fileContent.slice(0, startIndex);
925
964
  const afterContent = this.fileContent.slice(stopIndex);
926
- this.fileContent = addNewlines ? `${beforeContent}\n\n${content}\n${afterContent}` : `${beforeContent}${content}${afterContent}`;
965
+ this.fileContent = `${beforeContent}${layoutSpan(content, addNewlines)}${afterContent}`;
966
+ if (addNewlines) this.updatedSections.set(name, content);
927
967
  }
928
968
  }
929
969
  /**
970
+ * Formats the span of one section in isolation and splices it back.
971
+ *
972
+ * The content is formatted on its own and reassembled with the same
973
+ * surrounding newlines `updateSection` wrote, so the markers and every byte
974
+ * outside them survive untouched.
975
+ *
976
+ * The span is formatted only while its markers still bound exactly the text
977
+ * `updateSection` wrote. The markers are paired again here, after every
978
+ * section has been written, and a marker another section wrote can win that
979
+ * pairing; the text between such a pair is not this tool's to format.
980
+ * @param {string} name - The name of the section.
981
+ * @param {string} content - The content `updateSection` wrote, padded.
982
+ */
983
+ async formatSection(name, content) {
984
+ const [startIndex, stopIndex] = this.getTokenIndexes(name);
985
+ if (!startIndex || !stopIndex) return;
986
+ if (startIndex > stopIndex || this.fileContent.slice(startIndex, stopIndex) !== layoutSpan(content, true)) {
987
+ this.log.warn(`The '${name}' markers no longer bound the text written to them. Leaving the section unformatted`);
988
+ return;
989
+ }
990
+ const formatted = content === "" ? "" : (await formatMarkdown(content)).trim();
991
+ const span = formatted === "" ? "\n" : layoutSpan(formatted, true);
992
+ this.fileContent = `${this.fileContent.slice(0, startIndex)}${span}${this.fileContent.slice(stopIndex)}`;
993
+ }
994
+ /**
995
+ * Formats every span this editor replaced, one span at a time.
996
+ *
997
+ * Each span is located again before it is formatted, because formatting the
998
+ * previous one moves the indexes of the spans after it. A formatted span no
999
+ * longer holds the text `updateSection` wrote, so it is forgotten once
1000
+ * formatted.
1001
+ * @returns {Promise<void>}
1002
+ */
1003
+ async formatUpdatedSections() {
1004
+ for (const [name, content] of this.updatedSections) await this.formatSection(name, content);
1005
+ this.updatedSections.clear();
1006
+ }
1007
+ /**
930
1008
  * Dumps the modified content back to the README file.
931
- * @param {boolean} [prettier=true] - Run the result through prettier before
932
- * writing. Callers pass the resolved `pretty` input; it defaults to true so
933
- * constructing a ReadmeEditor directly keeps the formatting behaviour.
1009
+ * @param {boolean} [prettier=true] - Run the replaced spans through prettier
1010
+ * before writing. Callers pass the resolved `pretty` input; it defaults to
1011
+ * true so constructing a ReadmeEditor directly keeps the formatting
1012
+ * behaviour. Text outside the markers is never formatted, whatever this
1013
+ * flag says — see `docs/tool-contract.md`.
934
1014
  * @returns {Promise<void>}
935
1015
  */
936
1016
  async dumpToFile(prettier = true) {
937
- const content = prettier ? await formatMarkdown(this.fileContent) : this.fileContent;
1017
+ if (prettier) await this.formatUpdatedSections();
1018
+ const content = this.crlf ? this.fileContent.replaceAll("\n", "\r\n") : this.fileContent;
938
1019
  if (process.env.GITHUB_ACTIONS) core.setOutput("readme_after", content);
939
1020
  return fs.promises.writeFile(this.filePath, content, "utf8");
940
1021
  }
@@ -1489,10 +1570,12 @@ function loadConfig(log, providedConfig, configFilePath) {
1489
1570
  const config = providedConfig ?? new Provider();
1490
1571
  if (process.env.GITHUB_ACTION === "true") log.info("Running in GitHub action");
1491
1572
  config.argv(argvOptions);
1492
- if (configFilePath) if (fs.existsSync(configFilePath)) {
1493
- log.info(`Config file found: ${configFilePath}`);
1494
- config.file(configFilePath);
1495
- } else log.debug(`Config file not found: ${configFilePath}`);
1573
+ if (configFilePath) {
1574
+ if (fs.existsSync(configFilePath)) {
1575
+ log.info(`Config file found: ${configFilePath}`);
1576
+ config.file(configFilePath);
1577
+ } else log.debug(`Config file not found: ${configFilePath}`);
1578
+ }
1496
1579
  config.env({
1497
1580
  lowerCase: true,
1498
1581
  parseValues: true,
@@ -2229,6 +2312,13 @@ function updateTitle(sectionToken, inputs) {
2229
2312
  }
2230
2313
  //#endregion
2231
2314
  //#region src/sections/update-usage.ts
2315
+ /**
2316
+ * Renders the `usage` section: a fenced workflow snippet naming every action input,
2317
+ * each preceded by its description and default as YAML comments.
2318
+ * @param {ReadmeSection} sectionToken - The README marker pair to write the section into.
2319
+ * @param {Inputs} inputs - The parsed action metadata and the README editor to write with.
2320
+ * @returns {Promise<Record<string, string>>} The rendered section, keyed by its section token.
2321
+ */
2232
2322
  async function updateUsage(sectionToken, inputs) {
2233
2323
  const log = new LogTask(sectionToken);
2234
2324
  log.start();
@@ -2238,6 +2328,8 @@ async function updateUsage(sectionToken, inputs) {
2238
2328
  log.info(`Version string: ${versionString}`);
2239
2329
  const actionReference = `${actionName}@${versionString}`;
2240
2330
  const indent = " # ";
2331
+ const defaultLabel = "Default: ";
2332
+ const defaultHang = " ".repeat(9);
2241
2333
  const content = [];
2242
2334
  content.push("```yaml", `- uses: ${actionReference}`, " with:");
2243
2335
  const inp = inputs.action.inputs;
@@ -2264,7 +2356,11 @@ async function updateUsage(sectionToken, inputs) {
2264
2356
  if (input !== void 0) {
2265
2357
  if (!firstInput) content.push("");
2266
2358
  content.push(...descriptions[key]);
2267
- if (input.default !== void 0) content.push(`${indent}Default: ${input.default}`);
2359
+ if (input.default !== void 0) {
2360
+ const [firstLine, ...rest] = `${input.default}`.split(/\r\n|\n|\r/);
2361
+ content.push(`${indent}${defaultLabel}${firstLine}`.trimEnd());
2362
+ for (const line of rest) content.push(`${indent}${defaultHang}${line}`.trimEnd());
2363
+ }
2268
2364
  content.push(` ${key}: ''`);
2269
2365
  firstInput = false;
2270
2366
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "github-action-readme-generator",
3
3
  "displayName": "bitflight-devops/github-action-readme-generator",
4
- "version": "1.12.8",
4
+ "version": "2.0.0",
5
5
  "description": "The docs generator for GitHub Actions. Auto-syncs action.yml to README.md with 8 sections: inputs, outputs, usage, badges, branding & more. Works as CLI or GitHub Action.",
6
6
  "keywords": [
7
7
  "github-actions",
@@ -128,7 +128,7 @@
128
128
  "@semantic-release/github",
129
129
  "@semantic-release/git"
130
130
  ],
131
- "preset": "angular"
131
+ "preset": "conventionalcommits"
132
132
  },
133
133
  "dependencies": {
134
134
  "@actions/core": "^3.0.0",
@@ -149,13 +149,14 @@
149
149
  "@commitlint/prompt": "^21.0.1",
150
150
  "@semantic-release/changelog": "^7.0.0",
151
151
  "@semantic-release/exec": "^7.1.0",
152
- "@semantic-release/git": "^10.0.1",
152
+ "@semantic-release/git": "^11.0.1",
153
153
  "@tsconfig/node24": "^24.0.4",
154
154
  "@types/nconf": "^0.10.5",
155
155
  "@types/node": "^26.1.1",
156
- "@vitest/coverage-v8": "4.1.10",
156
+ "@vitest/coverage-v8": "4.1.11",
157
157
  "@voidzero-dev/vite-plus-core": "0.2.8",
158
158
  "commitizen": "^4.3.1",
159
+ "conventional-changelog-conventionalcommits": "^9.3.1",
159
160
  "conventional-commits": "^1.6.0",
160
161
  "cz-conventional-changelog": "^3.3.0",
161
162
  "dotenv": "^17.2.4",
@@ -168,7 +169,7 @@
168
169
  "semantic-release": "^25.0.2",
169
170
  "types-package-json": "^2.0.39",
170
171
  "typescript": "7.0.2",
171
- "vite-plus": "0.2.8"
172
+ "vite-plus": "0.3.1"
172
173
  },
173
174
  "engines": {
174
175
  "node": ">=24.19.0 <30.0.0",