From 638d63339a1f3b3abdece8ec0d4155ba38557e95 Mon Sep 17 00:00:00 2001 From: Xavier Krantz Date: Wed, 5 Nov 2025 17:56:14 +0100 Subject: [PATCH 1/2] feat: implement commit message templating with lodash support - Add detailed `message` option to commit configuration in README. - Expand README with configuration and example sections for commit message templates. - Add `lodash` and `@types/lodash` to dependencies in `package.json`. - Import `template` from `lodash` and create a `PluginConfig` type in `src/index.ts`. - Replace `unsafeParseAssets` with `unsafeParsePluginConfig` to handle both assets and optional message in `src/index.ts`. - Implement custom commit message templating with lodash in `prepare` function in `src/index.ts`. Signed-off-by: Xavier Krantz --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++---- package-lock.json | 26 ++++++++++++++++++++ package.json | 2 ++ src/index.ts | 45 +++++++++++++++++++++++++++++------ 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ae33c8a..a14d3aa 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,31 @@ npm install --save-dev --save-exact @semantic-release-extras/verified-git-commit ## Use -| Step | Description | -| -------- | -------------------------------------------------------------------------------------------------- | -| `assets` | List of assets to commit back to the release branch. Each asset will be updated in its own commit. | +| Step | Description | +| --------- | --------------------------------------------------------------------------------------------------- | +| `assets` | List of assets to commit back to the release branch. Each asset will be updated in its own commit. | +| `message` | The commit message template. Optional, defaults to `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}` | -For example: +### Configuration + +The `message` option uses [Lodash templates](https://lodash.com/docs#template) and supports the following variables: + +- `branch` - The branch name +- `lastRelease` - Previous release details with properties: + - `lastRelease.version` - Version of the previous release + - `lastRelease.gitTag` - Git tag of the previous release + - `lastRelease.gitHead` - Git hash of the previous release +- `nextRelease` - Current release info with properties: + - `nextRelease.version` - Version of the next release + - `nextRelease.gitTag` - Git tag of the next release + - `nextRelease.gitHead` - Git hash of the next release + - `nextRelease.notes` - Release notes for the next release + +**Note:** It is recommended to include `[skip ci]` in the commit message to not trigger a new build. Some CI services support the `[skip ci]` keyword only in the subject of the message. + +### Examples + +Basic configuration with default commit message: ```json { @@ -72,6 +92,38 @@ For example: } ``` +Custom commit message template: + +```json +{ + "plugins": [ + [ + "@semantic-release-extras/verified-git-commit", + { + "assets": ["CHANGELOG.md", "package.json"], + "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}" + } + ] + ] +} +``` + +Advanced template with branch name: + +```json +{ + "plugins": [ + [ + "@semantic-release-extras/verified-git-commit", + { + "assets": ["CHANGELOG.md"], + "message": "chore(release): ${nextRelease.version} on ${branch} [skip ci]\n\n${nextRelease.notes}" + } + ] + ] +} +``` + ## Acknowledgments Many thanks to @swinton for documenting the approach in [this gist]! diff --git a/package-lock.json b/package-lock.json index 7e3cfc8..b6f408f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@octokit/plugin-throttling": "^11.0.0", "@octokit/rest": "^22.0.0", + "lodash": "^4.17.21", "parse-github-repo-url": "1.4.1" }, "devDependencies": { + "@types/lodash": "^4.17.20", "@types/node": "24.10.0", "@types/parse-github-repo-url": "1.4.2", "@types/semantic-release": "20.0.6", @@ -177,6 +179,13 @@ "@octokit/openapi-types": "^25.1.0" } }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", @@ -226,6 +235,12 @@ } ] }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/parse-github-repo-url": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", @@ -370,6 +385,12 @@ "@octokit/openapi-types": "^25.1.0" } }, + "@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true + }, "@types/node": { "version": "24.10.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", @@ -409,6 +430,11 @@ "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==" }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "parse-github-repo-url": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", diff --git a/package.json b/package.json index ffa68ad..4ec7704 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,11 @@ "dependencies": { "@octokit/plugin-throttling": "^11.0.0", "@octokit/rest": "^22.0.0", + "lodash": "^4.17.21", "parse-github-repo-url": "1.4.1" }, "devDependencies": { + "@types/lodash": "^4.17.20", "@types/node": "24.10.0", "@types/parse-github-repo-url": "1.4.2", "@types/semantic-release": "20.0.6", diff --git a/src/index.ts b/src/index.ts index f1ff7af..ba5389a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import { Octokit } from "@octokit/rest"; import { throttling } from "@octokit/plugin-throttling"; import type { Context, PluginSpec } from "semantic-release"; import parseRepositoryUrl from "parse-github-repo-url"; +import { template } from "lodash"; const ThrottlingOctokit = Octokit.plugin(throttling); @@ -13,6 +14,11 @@ type RepositorySlug = { name: string; }; +type PluginConfig = { + assets: string[]; + message?: string; +}; + function readFileInBase64(path: string): string { return fs.readFileSync(path, { encoding: "base64" }); } @@ -56,7 +62,7 @@ function unsafeParseRepositorySlug(repositoryUrl: string): RepositorySlug { // The type PluginSpec is misleading here, `semantic-release --dry-run` // indicates this value is an object. -function unsafeParseAssets(pluginConfig: unknown): string[] { +function unsafeParsePluginConfig(pluginConfig: unknown): PluginConfig { if (typeof pluginConfig === "string") { throw new Error(`Expected plugin config to specify 'assets'`); } @@ -73,14 +79,32 @@ function unsafeParseAssets(pluginConfig: unknown): string[] { `Expected plugin config 'assets' to contain an array of strings` ); } - return assets.map((value) => unsafeParseString(value)); + + const parsedAssets = assets.map((value) => unsafeParseString(value)); + + // Parse optional message configuration + const result: PluginConfig = { + assets: parsedAssets, + }; + + if ("message" in config) { + if (typeof config.message === "string") { + result.message = config.message; + } else if (config.message !== undefined) { + throw new Error( + `Expected plugin config 'message' to be a string if provided` + ); + } + } + + return result; } function verifyConditions(pluginConfig: PluginSpec, context: Context) { const repositoryUrl = unsafeParseString(context.options?.repositoryUrl); unsafeParseRepositorySlug(repositoryUrl); unsafeParseString(context.env["GITHUB_TOKEN"]); - unsafeParseAssets(pluginConfig); + unsafeParsePluginConfig(pluginConfig); // TODO: test if github token has the right permissions, like // @semantic-release/git does } @@ -90,7 +114,7 @@ async function prepare(pluginConfig: PluginSpec, context: Context) { const branch = context.branch.name; const slug = unsafeParseRepositorySlug(repositoryUrl); const githubToken = unsafeParseString(context.env["GITHUB_TOKEN"]); - const assets = unsafeParseAssets(pluginConfig); + const config = unsafeParsePluginConfig(pluginConfig); const octokit = getOctokit(githubToken); const nextRelease = context.nextRelease; @@ -99,10 +123,17 @@ async function prepare(pluginConfig: PluginSpec, context: Context) { `Did not expect 'prepare' to be invoked with undefined 'nextRelease'` ); } - // This is the default commit message from @semantic-release/git - const message = `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`; - for (const path of assets) { + // Use custom message template if provided, otherwise use default from @semantic-release/git + const message = config.message + ? template(config.message)({ + branch: context.branch.name, + lastRelease: context.lastRelease, + nextRelease: context.nextRelease, + }) + : `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`; + + for (const path of config.assets) { const content = readFileInBase64(path); const sha = execSync(`git rev-parse ${branch}:${path}`, { encoding: "utf8", From 28684cb29cee38d5b7e71801b27448d654975cc7 Mon Sep 17 00:00:00 2001 From: Xavier Krantz Date: Wed, 5 Nov 2025 18:10:55 +0100 Subject: [PATCH 2/2] fixup --- package.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 4ec7704..7400571 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "", "main": "dist/index.js", "scripts": { - "build": "tsc --build ." + "build": "tsc --build .", + "prepare": "tsc --build .", + "prepublishOnly": "npm run build" }, "repository": { "type": "git", @@ -27,13 +29,13 @@ "@octokit/plugin-throttling": "^11.0.0", "@octokit/rest": "^22.0.0", "lodash": "^4.17.21", - "parse-github-repo-url": "1.4.1" + "parse-github-repo-url": "1.4.1", + "typescript": "5.9.3" }, "devDependencies": { "@types/lodash": "^4.17.20", "@types/node": "24.10.0", "@types/parse-github-repo-url": "1.4.2", - "@types/semantic-release": "20.0.6", - "typescript": "5.9.3" + "@types/semantic-release": "20.0.6" } }