Skip to content

Commit 46ae0a1

Browse files
committed
feat: add 'draftRelease' option
Fix semantic-release#275
1 parent 82160dd commit 46ae0a1

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
@@ -83,6 +83,7 @@ If you have actions that trigger on newly created releases, please use a generat
8383
| `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. | - |
8484
| `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}\` : "" %>']- |
8585
| `addReleases` | Will add release links to the GitHub Release. Can be `false`, `"bottom"` or `"top"`. See [addReleases](#addReleases). | `false` |
86+
| `draftRelease` | A boolean indicating if a GitHub Draft Release should be created instead of publishing an actual GitHub Release. | `false` |
8687

8788
#### proxy
8889

@@ -212,4 +213,4 @@ Valid values for this option are `false`, `"top"` or `"bottom"`.
212213

213214
##### addReleases example
214215

215-
See [The introducing PR](https://github.com/semantic-release/github/pull/282) for an example on how it will look.
216+
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 github = 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 github.repos.createRelease(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 github.repos.createRelease(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 github.repos.createRelease(draftRelease);
64+
data: {upload_url: uploadUrl, html_url: draftUrl, id: releaseId},
65+
} = await github.repos.createRelease(draftReleaseOptions);
5366

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

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

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
@@ -372,6 +372,95 @@ test.serial('Publish a release with an array of missing assets', async (t) => {
372372
t.true(github.isDone());
373373
});
374374

375+
test.serial('Publish a draft release', async (t) => {
376+
const owner = 'test_user';
377+
const repo = 'test_repo';
378+
const env = {GITHUB_TOKEN: 'github_token'};
379+
const pluginConfig = {draftRelease: true};
380+
const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
381+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
382+
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
383+
const releaseId = 1;
384+
const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`;
385+
const uploadUrl = `https://github.com${uploadUri}{?name,label}`;
386+
const branch = 'test_branch';
387+
388+
const github = authenticate(env)
389+
.post(`/repos/${owner}/${repo}/releases`, {
390+
tag_name: nextRelease.gitTag,
391+
target_commitish: branch,
392+
name: nextRelease.name,
393+
body: nextRelease.notes,
394+
draft: true,
395+
prerelease: false,
396+
})
397+
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl});
398+
399+
const result = await publish(pluginConfig, {
400+
cwd,
401+
env,
402+
options,
403+
branch: {name: branch, type: 'release', main: true},
404+
nextRelease,
405+
logger: t.context.logger,
406+
});
407+
408+
t.is(result.url, releaseUrl);
409+
t.deepEqual(t.context.log.args[0], ['Created GitHub draft release: %s', releaseUrl]);
410+
t.true(github.isDone());
411+
});
412+
413+
test.serial('Publish a draft release with one asset', async (t) => {
414+
const owner = 'test_user';
415+
const repo = 'test_repo';
416+
const env = {GITHUB_TOKEN: 'github_token'};
417+
const pluginConfig = {
418+
assets: [['**', '!**/*.txt'], {path: '.dotfile', label: 'A dotfile with no ext'}],
419+
draftRelease: true,
420+
};
421+
const nextRelease = {gitTag: 'v1.0.0', name: 'v1.0.0', notes: 'Test release note body'};
422+
const options = {repositoryUrl: `https://github.com/${owner}/${repo}.git`};
423+
const releaseUrl = `https://github.com/${owner}/${repo}/releases/${nextRelease.version}`;
424+
const assetUrl = `https://github.com/${owner}/${repo}/releases/download/${nextRelease.version}/.dotfile`;
425+
const releaseId = 1;
426+
const uploadUri = `/api/uploads/repos/${owner}/${repo}/releases/${releaseId}/assets`;
427+
const uploadUrl = `https://github.com${uploadUri}{?name,label}`;
428+
const branch = 'test_branch';
429+
430+
const github = authenticate(env)
431+
.post(`/repos/${owner}/${repo}/releases`, {
432+
tag_name: nextRelease.gitTag,
433+
target_commitish: branch,
434+
name: nextRelease.name,
435+
body: nextRelease.notes,
436+
draft: true,
437+
prerelease: false,
438+
})
439+
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl, id: releaseId});
440+
441+
const githubUpload = upload(env, {
442+
uploadUrl: 'https://github.com',
443+
contentLength: (await stat(path.resolve(cwd, '.dotfile'))).size,
444+
})
445+
.post(`${uploadUri}?name=${escape('.dotfile')}&label=${escape('A dotfile with no ext')}`)
446+
.reply(200, {browser_download_url: assetUrl});
447+
448+
const result = await publish(pluginConfig, {
449+
cwd,
450+
env,
451+
options,
452+
branch: {name: branch, type: 'release', main: true},
453+
nextRelease,
454+
logger: t.context.logger,
455+
});
456+
457+
t.is(result.url, releaseUrl);
458+
t.true(t.context.log.calledWith('Created GitHub draft release: %s', releaseUrl));
459+
t.true(t.context.log.calledWith('Published file %s', assetUrl));
460+
t.true(github.isDone());
461+
t.true(githubUpload.isDone());
462+
});
463+
375464
test.serial('Throw error without retries for 400 error', async (t) => {
376465
const owner = 'test_user';
377466
const repo = 'test_repo';

test/verify.test.js

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

419+
test.serial('Verify "draftRelease" is valid (true)', async (t) => {
420+
const owner = 'test_user';
421+
const repo = 'test_repo';
422+
const env = {GH_TOKEN: 'github_token'};
423+
const draftRelease = true;
424+
const github = authenticate(env)
425+
.get(`/repos/${owner}/${repo}`)
426+
.reply(200, {permissions: {push: true}});
427+
428+
await t.notThrowsAsync(
429+
verify(
430+
{draftRelease},
431+
{env, options: {repositoryUrl: `[email protected]:${owner}/${repo}.git`}, logger: t.context.logger}
432+
)
433+
);
434+
435+
t.true(github.isDone());
436+
});
437+
438+
test.serial('Verify "draftRelease" is valid (false)', async (t) => {
439+
const owner = 'test_user';
440+
const repo = 'test_repo';
441+
const env = {GH_TOKEN: 'github_token'};
442+
const draftRelease = false;
443+
const github = authenticate(env)
444+
.get(`/repos/${owner}/${repo}`)
445+
.reply(200, {permissions: {push: true}});
446+
447+
await t.notThrowsAsync(
448+
verify(
449+
{draftRelease},
450+
{env, options: {repositoryUrl: `[email protected]:${owner}/${repo}.git`}, logger: t.context.logger}
451+
)
452+
);
453+
454+
t.true(github.isDone());
455+
});
456+
419457
// https://github.com/semantic-release/github/issues/182
420458
test.serial('Verify if run in GitHub Action', async (t) => {
421459
const owner = 'test_user';
@@ -1139,3 +1177,25 @@ test.serial('Throw SemanticReleaseError if "addReleases" option is not a valid s
11391177
t.is(error.code, 'EINVALIDADDRELEASES');
11401178
t.true(github.isDone());
11411179
});
1180+
1181+
test.serial('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean (string)', async (t) => {
1182+
const owner = 'test_user';
1183+
const repo = 'test_repo';
1184+
const env = {GH_TOKEN: 'github_token'};
1185+
const draftRelease = 'test';
1186+
const github = authenticate(env)
1187+
.get(`/repos/${owner}/${repo}`)
1188+
.reply(200, {permissions: {push: true}});
1189+
1190+
const [error, ...errors] = await t.throwsAsync(
1191+
verify(
1192+
{draftRelease},
1193+
{env, options: {repositoryUrl: `https://github.com/${owner}/${repo}.git`}, logger: t.context.logger}
1194+
)
1195+
);
1196+
1197+
t.is(errors.length, 0);
1198+
t.is(error.name, 'SemanticReleaseError');
1199+
t.is(error.code, 'EINVALIDDRAFTRELEASE');
1200+
t.true(github.isDone());
1201+
});

0 commit comments

Comments
 (0)