Skip to content

Commit

Permalink
feat: add 'draftRelease' option (#379)
Browse files Browse the repository at this point in the history
Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
  • Loading branch information
BetaHuhn and gr2m committed May 28, 2023
1 parent 0f12010 commit 3c42e02
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 8 deletions.
3 changes: 2 additions & 1 deletion README.md
Expand Up @@ -89,6 +89,7 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are:
| `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. | - |
| `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` |

#### proxy

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

##### addReleases example

See [The introducing PR](https://github.com/semantic-release/github/pull/282) for an example on how it will look.
See [The introducing PR](https://github.com/semantic-release/github/pull/282) for an example on how it will look.
6 changes: 6 additions & 0 deletions lib/definitions/errors.js
Expand Up @@ -63,6 +63,12 @@ Your configuration for the \`releasedLabels\` option is \`${stringify(releasedLa
details: `The [addReleases option](${linkify('README.md#options')}) if defined, must be one of \`false|top|bottom\`.
Your configuration for the \`addReleases\` option is \`${stringify(addReleases)}\`.`,
}),
EINVALIDDRAFTRELEASE: ({draftRelease}) => ({
message: 'Invalid `draftRelease` option.',
details: `The [draftRelease option](${linkify('README.md#options')}) if defined, must be a \`Boolean\`.
Your configuration for the \`draftRelease\` option is \`${stringify(draftRelease)}\`.`,
}),
EINVALIDGITHUBURL: () => ({
message: 'The git repository URL is not a valid GitHub URL.',
Expand Down
31 changes: 25 additions & 6 deletions lib/publish.js
Expand Up @@ -18,7 +18,10 @@ module.exports = async (pluginConfig, context) => {
nextRelease: {name, gitTag, notes},
logger,
} = context;
const {githubToken, githubUrl, githubApiPathPrefix, proxy, assets} = resolveConfig(pluginConfig, context);
const {githubToken, githubUrl, githubApiPathPrefix, proxy, assets, draftRelease} = resolveConfig(
pluginConfig,
context
);
const {owner, repo} = parseGithubUrl(repositoryUrl);
const octokit = getClient({githubToken, githubUrl, githubApiPathPrefix, proxy});
const release = {
Expand All @@ -33,8 +36,20 @@ module.exports = async (pluginConfig, context) => {

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

// When there are no assets, we publish a release directly
const draftReleaseOptions = {...release, draft: true};

// When there are no assets, we publish a release directly.
if (!assets || assets.length === 0) {
// If draftRelease is true we create a draft release instead.
if (draftRelease) {
const {
data: {html_url: url, id: releaseId},
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftReleaseOptions);

logger.log('Created GitHub draft release: %s', url);
return {url, name: RELEASE_NAME, id: releaseId};
}

const {
data: {html_url: url, id: releaseId},
} = await octokit.request('POST /repos/{owner}/{repo}/releases', release);
Expand All @@ -45,11 +60,9 @@ module.exports = async (pluginConfig, context) => {

// We'll create a draft release, append the assets to it, and then publish it.
// This is so that the assets are available when we get a Github release event.
const draftRelease = {...release, draft: true};

const {
data: {upload_url: uploadUrl, id: releaseId},
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftRelease);
data: {upload_url: uploadUrl, html_url: draftUrl, id: releaseId},
} = await octokit.request('POST /repos/{owner}/{repo}/releases', draftReleaseOptions);

// Append assets to the release
const globbedAssets = await globAssets(context, assets);
Expand Down Expand Up @@ -98,6 +111,12 @@ module.exports = async (pluginConfig, context) => {
})
);

// If we want to create a draft we don't need to update the release again
if (draftRelease) {
logger.log('Created GitHub draft release: %s', draftUrl);
return {url: draftUrl, name: RELEASE_NAME, id: releaseId};
}

const {
data: {html_url: url},
} = await octokit.request('PATCH /repos/{owner}/{repo}/releases/{release_id}', {
Expand Down
2 changes: 2 additions & 0 deletions lib/resolve-config.js
Expand Up @@ -13,6 +13,7 @@ module.exports = (
assignees,
releasedLabels,
addReleases,
draftRelease,
},
{env}
) => ({
Expand All @@ -32,4 +33,5 @@ module.exports = (
? false
: castArray(releasedLabels),
addReleases: isNil(addReleases) ? false : addReleases,
draftRelease: isNil(draftRelease) ? false : draftRelease,
});
3 changes: 2 additions & 1 deletion lib/verify.js
@@ -1,4 +1,4 @@
const {isString, isPlainObject, isNil, isArray, isNumber} = require('lodash');
const {isString, isPlainObject, isNil, isArray, isNumber, isBoolean} = require('lodash');
const urlJoin = require('url-join');
const AggregateError = require('aggregate-error');
const parseGithubUrl = require('./parse-github-url');
Expand Down Expand Up @@ -27,6 +27,7 @@ const VALIDATORS = {
assignees: isArrayOf(isNonEmptyString),
releasedLabels: canBeDisabled(isArrayOf(isNonEmptyString)),
addReleases: canBeDisabled(oneOf(['bottom', 'top'])),
draftRelease: isBoolean,
};

module.exports = async (pluginConfig, context) => {
Expand Down
89 changes: 89 additions & 0 deletions test/publish.test.js
Expand Up @@ -325,6 +325,95 @@ test.serial('Publish a release with an array of missing assets', async (t) => {
t.true(github.isDone());
});

test.serial('Publish a draft release', async (t) => {
const owner = 'test_user';
const repo = 'test_repo';
const env = {GITHUB_TOKEN: 'github_token'};
const pluginConfig = {draftRelease: true};
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 github = authenticate(env)
.post(`/repos/${owner}/${repo}/releases`, {
tag_name: nextRelease.gitTag,
target_commitish: branch,
name: nextRelease.name,
body: nextRelease.notes,
draft: true,
prerelease: false,
})
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl});

const result = await publish(pluginConfig, {
cwd,
env,
options,
branch: {name: branch, type: 'release', main: true},
nextRelease,
logger: t.context.logger,
});

t.is(result.url, releaseUrl);
t.deepEqual(t.context.log.args[0], ['Created GitHub draft release: %s', releaseUrl]);
t.true(github.isDone());
});

test.serial('Publish a draft release with one asset', async (t) => {
const owner = 'test_user';
const repo = 'test_repo';
const env = {GITHUB_TOKEN: 'github_token'};
const pluginConfig = {
assets: [['**', '!**/*.txt'], {path: '.dotfile', label: 'A dotfile with no ext'}],
draftRelease: true,
};
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 assetUrl = `https://github.com/${owner}/${repo}/releases/download/${nextRelease.version}/.dotfile`;
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 github = authenticate(env)
.post(`/repos/${owner}/${repo}/releases`, {
tag_name: nextRelease.gitTag,
target_commitish: branch,
name: nextRelease.name,
body: nextRelease.notes,
draft: true,
prerelease: false,
})
.reply(200, {upload_url: uploadUrl, html_url: releaseUrl, id: releaseId});

const githubUpload = upload(env, {
uploadUrl: 'https://github.com',
contentLength: (await stat(path.resolve(cwd, '.dotfile'))).size,
})
.post(`${uploadUri}?name=${escape('.dotfile')}&label=${escape('A dotfile with no ext')}`)
.reply(200, {browser_download_url: assetUrl});

const result = await publish(pluginConfig, {
cwd,
env,
options,
branch: {name: branch, type: 'release', main: true},
nextRelease,
logger: t.context.logger,
});

t.is(result.url, releaseUrl);
t.true(t.context.log.calledWith('Created GitHub draft release: %s', releaseUrl));
t.true(t.context.log.calledWith('Published file %s', assetUrl));
t.true(github.isDone());
t.true(githubUpload.isDone());
});

test.serial(
'Publish a release when env.GITHUB_URL is set to https://github.com (Default in GitHub Actions, #268)',
async (t) => {
Expand Down
60 changes: 60 additions & 0 deletions test/verify.test.js
Expand Up @@ -421,6 +421,44 @@ test.serial('Verify "addReleases" is valid (false)', async (t) => {
t.true(github.isDone());
});

test.serial('Verify "draftRelease" is valid (true)', async (t) => {
const owner = 'test_user';
const repo = 'test_repo';
const env = {GH_TOKEN: 'github_token'};
const draftRelease = true;
const github = authenticate(env)
.get(`/repos/${owner}/${repo}`)
.reply(200, {permissions: {push: true}});

await t.notThrowsAsync(
verify(
{draftRelease},
{env, options: {repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`}, logger: t.context.logger}
)
);

t.true(github.isDone());
});

test.serial('Verify "draftRelease" is valid (false)', async (t) => {
const owner = 'test_user';
const repo = 'test_repo';
const env = {GH_TOKEN: 'github_token'};
const draftRelease = false;
const github = authenticate(env)
.get(`/repos/${owner}/${repo}`)
.reply(200, {permissions: {push: true}});

await t.notThrowsAsync(
verify(
{draftRelease},
{env, options: {repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`}, logger: t.context.logger}
)
);

t.true(github.isDone());
});

// https://github.com/semantic-release/github/issues/182
test.serial('Verify if run in GitHub Action', async (t) => {
const owner = 'test_user';
Expand Down Expand Up @@ -1148,3 +1186,25 @@ test.serial('Throw SemanticReleaseError if "addReleases" option is not a valid s
t.is(error.code, 'EINVALIDADDRELEASES');
t.true(github.isDone());
});

test.serial('Throw SemanticReleaseError if "draftRelease" option is not a valid boolean (string)', async (t) => {
const owner = 'test_user';
const repo = 'test_repo';
const env = {GH_TOKEN: 'github_token'};
const draftRelease = 'test';
const github = authenticate(env)
.get(`/repos/${owner}/${repo}`)
.reply(200, {permissions: {push: true}});

const [error, ...errors] = await t.throwsAsync(
verify(
{draftRelease},
{env, options: {repositoryUrl: `https://github.com/${owner}/${repo}.git`}, logger: t.context.logger}
)
);

t.is(errors.length, 0);
t.is(error.name, 'SemanticReleaseError');
t.is(error.code, 'EINVALIDDRAFTRELEASE');
t.true(github.isDone());
});

0 comments on commit 3c42e02

Please sign in to comment.