Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add 'draftRelease' option #379

Merged
merged 3 commits into from
May 28, 2023
Merged
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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());
});