From e913ee5105e8a2373e0a403fddbeb8dcd3abb31f Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Thu, 21 Sep 2023 23:45:47 -0700 Subject: [PATCH] feat: Add support for customizing release body and name --- README.md | 2 + lib/definitions/errors.js | 26 ++++++++ lib/publish.js | 8 ++- lib/resolve-config.js | 8 +++ lib/verify.js | 2 + test/publish.test.js | 126 ++++++++++++++++++++++++++++++++++++++ test/verify.test.js | 72 ++++++++++++++++++++++ 7 files changed, 241 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 51b4eecbb..6e864ea00 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are: | `releasedLabels` | The [labels](https://help.github.com/articles/about-labels) to add to each issue and pull request resolved by the release. Set to `false` to not add any label. See [releasedLabels](#releasedlabels). | `['released<%= nextRelease.channel ? \` on @\${nextRelease.channel}\` : "" %>']- | | `addReleases` | Will add release links to the GitHub Release. Can be `false`, `"bottom"` or `"top"`. See [addReleases](#addReleases). | `false` | | `draftRelease` | A boolean indicating if a GitHub Draft Release should be created instead of publishing an actual GitHub Release. | `false` | +| `releaseNameTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's name | `<%= nextverison.name %>` | +| `releaseBodyTemplate` | A [Lodash template](https://lodash.com/docs#template) to customize the github release's body | `<%= nextverison.notes %>` | #### proxy diff --git a/lib/definitions/errors.js b/lib/definitions/errors.js index 236834226..31afa332a 100644 --- a/lib/definitions/errors.js +++ b/lib/definitions/errors.js @@ -195,3 +195,29 @@ export function ENOGHTOKEN({ owner, repo }) { Please make sure to create a [GitHub personal token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line) and to set it in the \`GH_TOKEN\` or \`GITHUB_TOKEN\` environment variable on your CI environment. The token must allow to push to the repository ${owner}/${repo}.`, }; } + +export function EINVALIDRELEASEBODYTEMPLATE({ releaseBodyTemplate }) { + return { + message: "Invalid `releaseBodyTemplate` option.", + details: `The [releaseBodyTemplate option](${linkify( + "README.md#releaseBodyTemplate", + )}) must be a non empty \`String\`. + +Your configuration for the \`releaseBodyTemplate\` option is \`${stringify( + releaseBodyTemplate, + )}\`.`, + }; +} + +export function EINVALIDRELEASENAMETEMPLATE({ releaseNameTemplate }) { + return { + message: "Invalid `releaseNameTemplate` option.", + details: `The [releaseNameTemplate option](${linkify( + "README.md#releaseNameTemplate", + )}) must be a non empty \`String\`. + +Your configuration for the \`releaseNameTemplate\` option is \`${stringify( + releaseNameTemplate, + )}\`.`, + }; +} diff --git a/lib/publish.js b/lib/publish.js index 07a6659eb..e6e7755d3 100644 --- a/lib/publish.js +++ b/lib/publish.js @@ -19,7 +19,7 @@ export default async function publish(pluginConfig, context, { Octokit }) { cwd, options: { repositoryUrl }, branch, - nextRelease: { name, gitTag, notes }, + nextRelease: { gitTag }, logger, } = context; const { @@ -29,6 +29,8 @@ export default async function publish(pluginConfig, context, { Octokit }) { proxy, assets, draftRelease, + releaseNameTemplate, + releaseBodyTemplate, } = resolveConfig(pluginConfig, context); const { owner, repo } = parseGithubUrl(repositoryUrl); const octokit = new Octokit( @@ -44,8 +46,8 @@ export default async function publish(pluginConfig, context, { Octokit }) { repo, tag_name: gitTag, target_commitish: branch.name, - name, - body: notes, + name: template(releaseNameTemplate)(context), + body: template(releaseBodyTemplate)(context), prerelease: isPrerelease(branch), }; diff --git a/lib/resolve-config.js b/lib/resolve-config.js index aa823dd8a..e4a7d2a8e 100644 --- a/lib/resolve-config.js +++ b/lib/resolve-config.js @@ -14,6 +14,8 @@ export default function resolveConfig( releasedLabels, addReleases, draftRelease, + releaseNameTemplate, + releaseBodyTemplate, }, { env }, ) { @@ -44,5 +46,11 @@ export default function resolveConfig( : castArray(releasedLabels), addReleases: isNil(addReleases) ? false : addReleases, draftRelease: isNil(draftRelease) ? false : draftRelease, + releaseBodyTemplate: !isNil(releaseBodyTemplate) + ? releaseBodyTemplate + : "<%= nextRelease.notes %>", + releaseNameTemplate: !isNil(releaseNameTemplate) + ? releaseNameTemplate + : "<%= nextRelease.name %>", }; } diff --git a/lib/verify.js b/lib/verify.js index d171f1b1f..e7768c033 100644 --- a/lib/verify.js +++ b/lib/verify.js @@ -45,6 +45,8 @@ const VALIDATORS = { releasedLabels: canBeDisabled(isArrayOf(isNonEmptyString)), addReleases: canBeDisabled(oneOf(["bottom", "top"])), draftRelease: isBoolean, + releaseBodyTemplate: isNonEmptyString, + releaseNameTemplate: isNonEmptyString, }; export default async function verify(pluginConfig, context, { Octokit }) { diff --git a/test/publish.test.js b/test/publish.test.js index 9f948d013..e08aa16c5 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -720,3 +720,129 @@ test("Publish a release when env.GITHUB_URL is set to https://github.com (Defaul ]); t.true(fetch.done()); }); + +test("Publish a custom release body", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const pluginConfig = { + releaseBodyTemplate: + "To install this run npm install package@<%= nextRelease.name %>\n\n<%= nextRelease.notes %>", + }; + const nextRelease = { + gitTag: "v1.0.0", + name: "v1.0.0", + notes: "Test release note body", + }; + const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; + const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`; + const releaseId = 1; + const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`; + const uploadUrl = `https://github.com${uploadUri}{?name,label}`; + const branch = "test_branch"; + + const fetch = fetchMock.sandbox().postOnce( + `https://api.github.local/repos/${owner}/${repo}/releases`, + { + upload_url: uploadUrl, + html_url: releaseUrl, + }, + { + body: { + tag_name: nextRelease.gitTag, + target_commitish: branch, + name: nextRelease.name, + body: `To install this run npm install package@${nextRelease.name}\n\n${nextRelease.notes}`, + prerelease: false, + }, + }, + ); + + const result = await publish( + pluginConfig, + { + cwd, + env, + options, + branch: { name: branch, type: "release", main: true }, + nextRelease, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.is(result.url, releaseUrl); + t.deepEqual(t.context.log.args[0], [ + "Published GitHub release: %s", + releaseUrl, + ]); + t.true(fetch.done()); +}); + +test("Publish a custom release name", async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GITHUB_TOKEN: "github_token" }; + const pluginConfig = { + releaseNameTemplate: + "omg its the best release: <%= nextRelease.name %> 🌈🌈", + }; + const nextRelease = { + gitTag: "v1.0.0", + name: "v1.0.0", + notes: "Test release note body", + }; + const options = { repositoryUrl: `https://github.com/${owner}/${repo}.git` }; + const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`; + const releaseId = 1; + const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`; + const uploadUrl = `https://github.com${uploadUri}{?name,label}`; + const branch = "test_branch"; + + const fetch = fetchMock.sandbox().postOnce( + `https://api.github.local/repos/${owner}/${repo}/releases`, + { + upload_url: uploadUrl, + html_url: releaseUrl, + }, + { + body: { + tag_name: nextRelease.gitTag, + target_commitish: branch, + name: `omg its the best release: ${nextRelease.name} 🌈🌈`, + body: nextRelease.notes, + prerelease: false, + }, + }, + ); + + const result = await publish( + pluginConfig, + { + cwd, + env, + options, + branch: { name: branch, type: "release", main: true }, + nextRelease, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ); + + t.is(result.url, releaseUrl); + t.deepEqual(t.context.log.args[0], [ + "Published GitHub release: %s", + releaseUrl, + ]); + t.true(fetch.done()); +}); diff --git a/test/verify.test.js b/test/verify.test.js index 4ecc04b4c..2120dc0de 100644 --- a/test/verify.test.js +++ b/test/verify.test.js @@ -2050,3 +2050,75 @@ test('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean t.is(error.code, "EINVALIDDRAFTRELEASE"); t.true(fetch.done()); }); + +test('Throw SemanticReleaseError if "releaseBodyTemplate" option is an empty string', async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GH_TOKEN: "github_token" }; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + permissions: { push: true }, + }); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + { releaseBodyTemplate: "" }, + { + env, + options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EINVALIDRELEASEBODYTEMPLATE"); + t.true(fetch.done()); +}); + +test('Throw SemanticReleaseError if "releaseNameTemplate" option is an empty string', async (t) => { + const owner = "test_user"; + const repo = "test_repo"; + const env = { GH_TOKEN: "github_token" }; + + const fetch = fetchMock + .sandbox() + .getOnce(`https://api.github.local/repos/${owner}/${repo}`, { + permissions: { push: true }, + }); + + const { + errors: [error, ...errors], + } = await t.throwsAsync( + verify( + { releaseNameTemplate: "" }, + { + env, + options: { repositoryUrl: `https://github.com/${owner}/${repo}.git` }, + logger: t.context.logger, + }, + { + Octokit: TestOctokit.defaults((options) => ({ + ...options, + request: { ...options.request, fetch }, + })), + }, + ), + ); + + t.is(errors.length, 0); + t.is(error.name, "SemanticReleaseError"); + t.is(error.code, "EINVALIDRELEASENAMETEMPLATE"); + t.true(fetch.done()); +});