Skip to content

Commit 3c42e02

Browse files
BetaHuhngr2m
andauthored
feat: add 'draftRelease' option (#379)
Co-authored-by: Gregor Martynus <[email protected]>
1 parent 0f12010 commit 3c42e02

File tree

7 files changed

+186
-8
lines changed

7 files changed

+186
-8
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are:
8989
| `assignees` | The [assignees](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users) to add to the issue created when a release fails. | - |
9090
| `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}\` : "" %>']- |
9191
| `addReleases` | Will add release links to the GitHub Release. Can be `false`, `"bottom"` or `"top"`. See [addReleases](#addReleases). | `false` |
92+
| `draftRelease` | A boolean indicating if a GitHub Draft Release should be created instead of publishing an actual GitHub Release. | `false` |
9293

9394
#### proxy
9495

@@ -218,4 +219,4 @@ Valid values for this option are `false`, `"top"` or `"bottom"`.
218219

219220
##### addReleases example
220221

221-
See [The introducing PR](https://github.com/semantic-release/github/pull/282) for an example on how it will look.
222+
See [The introducing PR](https://github.com/semantic-release/github/pull/282) for an example on how it will look.

lib/definitions/errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ Your configuration for the \`releasedLabels\` option is \`${stringify(releasedLa
6363
details: `The [addReleases option](${linkify('README.md#options')}) if defined, must be one of \`false|top|bottom\`.
6464
6565
Your configuration for the \`addReleases\` option is \`${stringify(addReleases)}\`.`,
66+
}),
67+
EINVALIDDRAFTRELEASE: ({draftRelease}) => ({
68+
message: 'Invalid `draftRelease` option.',
69+
details: `The [draftRelease option](${linkify('README.md#options')}) if defined, must be a \`Boolean\`.
70+
71+
Your configuration for the \`draftRelease\` option is \`${stringify(draftRelease)}\`.`,
6672
}),
6773
EINVALIDGITHUBURL: () => ({
6874
message: 'The git repository URL is not a valid GitHub URL.',

lib/publish.js

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ module.exports = async (pluginConfig, context) => {
1818
nextRelease: {name, gitTag, notes},
1919
logger,
2020
} = context;
21-
const {githubToken, githubUrl, githubApiPathPrefix, proxy, assets} = resolveConfig(pluginConfig, context);
21+
const {githubToken, githubUrl, githubApiPathPrefix, proxy, assets, draftRelease} = resolveConfig(
22+
pluginConfig,
23+
context
24+
);
2225
const {owner, repo} = parseGithubUrl(repositoryUrl);
2326
const octokit = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
2427
const release = {
@@ -33,8 +36,20 @@ module.exports = async (pluginConfig, context) => {
3336

3437
debug('release object: %O', release);
3538

36-
// When there are no assets, we publish a release directly
39+
const draftReleaseOptions = {...release, draft: true};
40+
41+
// When there are no assets, we publish a release directly.
3742
if (!assets || assets.length === 0) {
43+
// If draftRelease is true we create a draft release instead.
44+
if (draftRelease) {
45+
const {
46+
data: {html_url: url, id: releaseId},
47+
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftReleaseOptions);
48+
49+
logger.log('Created GitHub draft release: %s', url);
50+
return {url, name: RELEASE_NAME, id: releaseId};
51+
}
52+
3853
const {
3954
data: {html_url: url, id: releaseId},
4055
} = await octokit.request('POST /repos/{owner}/{repo}/releases', release);
@@ -45,11 +60,9 @@ module.exports = async (pluginConfig, context) => {
4560

4661
// We'll create a draft release, append the assets to it, and then publish it.
4762
// This is so that the assets are available when we get a Github release event.
48-
const draftRelease = {...release, draft: true};
49-
5063
const {
51-
data: {upload_url: uploadUrl, id: releaseId},
52-
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftRelease);
64+
data: {upload_url: uploadUrl, html_url: draftUrl, id: releaseId},
65+
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftReleaseOptions);
5366

5467
// Append assets to the release
5568
const globbedAssets = await globAssets(context, assets);
@@ -98,6 +111,12 @@ module.exports = async (pluginConfig, context) => {
98111
})
99112
);
100113

114+
// If we want to create a draft we don't need to update the release again
115+
if (draftRelease) {
116+
logger.log('Created GitHub draft release: %s', draftUrl);
117+
return {url: draftUrl, name: RELEASE_NAME, id: releaseId};
118+
}
119+
101120
const {
102121
data: {html_url: url},
103122
} = await octokit.request('PATCH /repos/{owner}/{repo}/releases/{release_id}', {

lib/resolve-config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = (
1313
assignees,
1414
releasedLabels,
1515
addReleases,
16+
draftRelease,
1617
},
1718
{env}
1819
) => ({
@@ -32,4 +33,5 @@ module.exports = (
3233
? false
3334
: castArray(releasedLabels),
3435
addReleases: isNil(addReleases) ? false : addReleases,
36+
draftRelease: isNil(draftRelease) ? false : draftRelease,
3537
});

lib/verify.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const {isString, isPlainObject, isNil, isArray, isNumber} = require('lodash');
1+
const {isString, isPlainObject, isNil, isArray, isNumber, isBoolean} = require('lodash');
22
const urlJoin = require('url-join');
33
const AggregateError = require('aggregate-error');
44
const parseGithubUrl = require('./parse-github-url');
@@ -27,6 +27,7 @@ const VALIDATORS = {
2727
assignees: isArrayOf(isNonEmptyString),
2828
releasedLabels: canBeDisabled(isArrayOf(isNonEmptyString)),
2929
addReleases: canBeDisabled(oneOf(['bottom', 'top'])),
30+
draftRelease: isBoolean,
3031
};
3132

3233
module.exports = async (pluginConfig, context) => {

test/publish.test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,95 @@ test.serial('Publish a release with an array of missing assets', async (t) => {
325325
t.true(github.isDone());
326326
});
327327

328+
test.serial('Publish a draft release', async (t) => {
329+
const owner = 'test_user';
330+
const repo = 'test_repo';
331+
const env = {GITHUB_TOKEN: 'github_token'};
332+
const pluginConfig = {draftRelease: true};
333+
const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
334+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
335+
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
336+
const releaseId = 1;
337+
const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`;
338+
const uploadUrl = `https://github.com${uploadUri}{?name,label}`;
339+
const branch = 'test_branch';
340+
341+
const github = authenticate(env)
342+
.post(`/repos/${owner}/${repo}/releases`, {
343+
tag_name: nextRelease.gitTag,
344+
target_commitish: branch,
345+
name: nextRelease.name,
346+
body: nextRelease.notes,
347+
draft: true,
348+
prerelease: false,
349+
})
350+
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl});
351+
352+
const result = await publish(pluginConfig, {
353+
cwd,
354+
env,
355+
options,
356+
branch: {name: branch, type: 'release', main: true},
357+
nextRelease,
358+
logger: t.context.logger,
359+
});
360+
361+
t.is(result.url, releaseUrl);
362+
t.deepEqual(t.context.log.args[0], ['Created GitHub draft release: %s', releaseUrl]);
363+
t.true(github.isDone());
364+
});
365+
366+
test.serial('Publish a draft release with one asset', async (t) => {
367+
const owner = 'test_user';
368+
const repo = 'test_repo';
369+
const env = {GITHUB_TOKEN: 'github_token'};
370+
const pluginConfig = {
371+
assets: [['**', '!**/*.txt'], {path: '.dotfile', label: 'A dotfile with no ext'}],
372+
draftRelease: true,
373+
};
374+
const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
375+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
376+
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
377+
const assetUrl = `https://github.com/${owner}/${repo}/releases/download/${nextRelease.version}/.dotfile`;
378+
const releaseId = 1;
379+
const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`;
380+
const uploadUrl = `https://github.com${uploadUri}{?name,label}`;
381+
const branch = 'test_branch';
382+
383+
const github = authenticate(env)
384+
.post(`/repos/${owner}/${repo}/releases`, {
385+
tag_name: nextRelease.gitTag,
386+
target_commitish: branch,
387+
name: nextRelease.name,
388+
body: nextRelease.notes,
389+
draft: true,
390+
prerelease: false,
391+
})
392+
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl, id: releaseId});
393+
394+
const githubUpload = upload(env, {
395+
uploadUrl: 'https://github.com',
396+
contentLength: (await stat(path.resolve(cwd, '.dotfile'))).size,
397+
})
398+
.post(`${uploadUri}?name=${escape('.dotfile')}&label=${escape('A dotfile with no ext')}`)
399+
.reply(200, {browser_download_url: assetUrl});
400+
401+
const result = await publish(pluginConfig, {
402+
cwd,
403+
env,
404+
options,
405+
branch: {name: branch, type: 'release', main: true},
406+
nextRelease,
407+
logger: t.context.logger,
408+
});
409+
410+
t.is(result.url, releaseUrl);
411+
t.true(t.context.log.calledWith('Created GitHub draft release: %s', releaseUrl));
412+
t.true(t.context.log.calledWith('Published file %s', assetUrl));
413+
t.true(github.isDone());
414+
t.true(githubUpload.isDone());
415+
});
416+
328417
test.serial(
329418
'Publish a release when env.GITHUB_URL is set to https://github.com (Default in GitHub Actions, #268)',
330419
async (t) => {

test/verify.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,44 @@ test.serial('Verify "addReleases" is valid (false)', async (t) => {
421421
t.true(github.isDone());
422422
});
423423

424+
test.serial('Verify "draftRelease" is valid (true)', async (t) => {
425+
const owner = 'test_user';
426+
const repo = 'test_repo';
427+
const env = {GH_TOKEN: 'github_token'};
428+
const draftRelease = true;
429+
const github = authenticate(env)
430+
.get(`/repos/${owner}/${repo}`)
431+
.reply(200, {permissions: {push: true}});
432+
433+
await t.notThrowsAsync(
434+
verify(
435+
{draftRelease},
436+
{env, options: {repositoryUrl: `[email protected]:${owner}/${repo}.git`}, logger: t.context.logger}
437+
)
438+
);
439+
440+
t.true(github.isDone());
441+
});
442+
443+
test.serial('Verify "draftRelease" is valid (false)', async (t) => {
444+
const owner = 'test_user';
445+
const repo = 'test_repo';
446+
const env = {GH_TOKEN: 'github_token'};
447+
const draftRelease = false;
448+
const github = authenticate(env)
449+
.get(`/repos/${owner}/${repo}`)
450+
.reply(200, {permissions: {push: true}});
451+
452+
await t.notThrowsAsync(
453+
verify(
454+
{draftRelease},
455+
{env, options: {repositoryUrl: `[email protected]:${owner}/${repo}.git`}, logger: t.context.logger}
456+
)
457+
);
458+
459+
t.true(github.isDone());
460+
});
461+
424462
// https://github.com/semantic-release/github/issues/182
425463
test.serial('Verify if run in GitHub Action', async (t) => {
426464
const owner = 'test_user';
@@ -1148,3 +1186,25 @@ test.serial('Throw SemanticReleaseError if "addReleases" option is not a valid s
11481186
t.is(error.code, 'EINVALIDADDRELEASES');
11491187
t.true(github.isDone());
11501188
});
1189+
1190+
test.serial('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean (string)', async (t) => {
1191+
const owner = 'test_user';
1192+
const repo = 'test_repo';
1193+
const env = {GH_TOKEN: 'github_token'};
1194+
const draftRelease = 'test';
1195+
const github = authenticate(env)
1196+
.get(`/repos/${owner}/${repo}`)
1197+
.reply(200, {permissions: {push: true}});
1198+
1199+
const [error, ...errors] = await t.throwsAsync(
1200+
verify(
1201+
{draftRelease},
1202+
{env, options: {repositoryUrl: `https://github.com/${owner}/${repo}.git`}, logger: t.context.logger}
1203+
)
1204+
);
1205+
1206+
t.is(errors.length, 0);
1207+
t.is(error.name, 'SemanticReleaseError');
1208+
t.is(error.code, 'EINVALIDDRAFTRELEASE');
1209+
t.true(github.isDone());
1210+
});

0 commit comments

Comments
 (0)