Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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]!
Expand Down
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -26,12 +28,14 @@
"dependencies": {
"@octokit/plugin-throttling": "^11.0.0",
"@octokit/rest": "^22.0.0",
"parse-github-repo-url": "1.4.1"
"lodash": "^4.17.21",
"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"
}
}
45 changes: 38 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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" });
}
Expand Down Expand Up @@ -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'`);
}
Expand All @@ -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
}
Expand All @@ -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;
Expand All @@ -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",
Expand Down