Skip to content

Commit e3eba37

Browse files
authored
feat: add --force-tag option to explicitly create git tags for releases. (#2627)
1 parent 9393f4b commit e3eba37

10 files changed

Lines changed: 313 additions & 29 deletions

File tree

__snapshots__/cli.js

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ Options:
5858
with the release for future tag creation upon
5959
"un-drafting" the release.
6060
[boolean] [default: false]
61+
--force-tag-creation Force the creation of a Git tag for the release.
62+
[boolean] [default: false]
6163
--prerelease mark release that have prerelease versions as as
6264
a prerelease on Github[boolean] [default: false]
6365
--label comma-separated list of labels to remove to from
@@ -117,41 +119,43 @@ release-please manifest-release
117119
create releases/tags from last release-PR using a manifest file
118120
119121
Options:
120-
--help Show help [boolean]
121-
--version Show version number [boolean]
122-
--debug print verbose errors (use only for local debugging).
123-
[boolean] [default: false]
124-
--trace print extra verbose errors (use only for local debugging).
122+
--help Show help [boolean]
123+
--version Show version number [boolean]
124+
--debug print verbose errors (use only for local debugging).
125125
[boolean] [default: false]
126-
--plugin load plugin named release-please-<plugin-name>
126+
--trace print extra verbose errors (use only for local
127+
debugging). [boolean] [default: false]
128+
--plugin load plugin named release-please-<plugin-name>
127129
[array] [default: []]
128-
--token GitHub token with repo write permissions
129-
--api-url URL to use when making API requests
130+
--token GitHub token with repo write permissions
131+
--api-url URL to use when making API requests
130132
[string] [default: "https://api.github.com"]
131-
--graphql-url URL to use when making GraphQL requests
133+
--graphql-url URL to use when making GraphQL requests
132134
[string] [default: "https://api.github.com"]
133-
--default-branch The branch to open release PRs against and tag releases on
134-
[deprecated: use --target-branch instead] [string]
135-
--target-branch The branch to open release PRs against and tag releases on
136-
[string]
137-
--repo-url GitHub URL to generate release for [required]
138-
--dry-run Prepare but do not take action [boolean] [default: false]
139-
--draft mark release as a draft. no tag is created but tag_name and
140-
target_commitish are associated with the release for future
141-
tag creation upon "un-drafting" the release.
135+
--default-branch The branch to open release PRs against and tag releases
136+
on [deprecated: use --target-branch instead] [string]
137+
--target-branch The branch to open release PRs against and tag releases
138+
on [string]
139+
--repo-url GitHub URL to generate release for [required]
140+
--dry-run Prepare but do not take action[boolean] [default: false]
141+
--draft mark release as a draft. no tag is created but tag_name
142+
and target_commitish are associated with the release for
143+
future tag creation upon "un-drafting" the release.
142144
[boolean] [default: false]
143-
--prerelease mark release that have prerelease versions as as a
144-
prerelease on Github [boolean] [default: false]
145-
--label comma-separated list of labels to remove to from release PR
146-
[default: "autorelease: pending"]
147-
--release-label set a pull request label other than "autorelease: tagged"
148-
[string] [default: "autorelease: tagged"]
149-
--snapshot-label set a java snapshot pull request label other than
150-
"autorelease: snapshot"
145+
--force-tag-creation Force the creation of a Git tag for the release.
146+
[boolean] [default: false]
147+
--prerelease mark release that have prerelease versions as as a
148+
prerelease on Github [boolean] [default: false]
149+
--label comma-separated list of labels to remove to from release
150+
PR [default: "autorelease: pending"]
151+
--release-label set a pull request label other than "autorelease:
152+
tagged" [string] [default: "autorelease: tagged"]
153+
--snapshot-label set a java snapshot pull request label other than
154+
"autorelease: snapshot"
151155
[string] [default: "autorelease: snapshot"]
152-
--config-file where can the config file be found in the project?
156+
--config-file where can the config file be found in the project?
153157
[default: "release-please-config.json"]
154-
--manifest-file where can the manifest file be found in the project?
158+
--manifest-file where can the manifest file be found in the project?
155159
[default: ".release-please-manifest.json"]
156160
`
157161

__snapshots__/github.js

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Extra options:
5454
| `--prerelease-type` | `string` | Configuration option for the prerelease versioning strategy. If prerelease strategy used and type set, will set the prerelease part of the version to the provided value in case prerelease part is not present. |
5555
| `--draft` | `boolean` | If set, create releases as drafts |
5656
| `--prerelease` | `boolean` | If set, create releases that are pre-major or pre-release version marked as pre-release on Github |
57+
| `--force-tag-creation` | `boolean` | Force the creation of a Git tag for the release. Useful when `--draft` is enabled, because GitHub does not create a Git tag for draft releases until they are published. This causes release-please to fail to find the previous release, potentially generating incorrect changelogs. Setting this option ensures the tag is created immediately. |
5758
| `--draft-pull-request` | `boolean` | If set, create pull requests as drafts |
5859
| `--label` | `string` | Comma-separated list of labels to apply to the release pull requests. Defaults to `autorelease: pending` |
5960
| `--release-label` | `string` | Comma-separated list of labels to apply to the pull request after the release has been tagged. Defaults to `autorelease: tagged` |
@@ -158,6 +159,7 @@ need to specify your release options:
158159
| `--pull-request-footer` | `string` | Override the pull request footer. Defaults to `This PR was generated with Release Please. See documentation.` |
159160
| `--draft` | `boolean` | If set, create releases as drafts |
160161
| `--prerelease` | `boolean` | If set, create releases that are pre-major or pre-release version marked as pre-release on Github|
162+
| `--force-tag-creation` | `boolean` | Force the creation of a Git tag for the release. Useful when `--draft` is enabled, because GitHub does not create a Git tag for draft releases until they are published. This causes release-please to fail to find the previous release, potentially generating incorrect changelogs. Setting this option ensures the tag is created immediately. |
161163
| `--label` | `string` | Comma-separated list of labels to apply to the release pull requests. Defaults to `autorelease: pending` |
162164
| `--release-label` | `string` | Comma-separated list of labels to apply to the pull request after the release has been tagged. Defaults to `autorelease: tagged` |
163165
| `--include-v-in-tags` | `boolean` | Include "v" in tag versions. Defaults to `true`. |

docs/manifest-releaser.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,18 @@ defaults (those are documented in comments)
197197
// when `manifest-release` creates GitHub Releases per package, create
198198
// those as "Prerelease" releases that have pre-major or prerelease versions.
199199
// absence defaults to false and all versions are fully Published.
200-
"prerelease": true
200+
"prerelease": true,
201+
202+
// Force the creation of a Git tag for the release. This is particularly
203+
// useful when `draft` is enabled. By default, GitHub does not create a Git
204+
// tag for draft releases until they are published. This "lazy tag creation"
205+
// behavior causes release-please to fail to find the previous release when
206+
// running subsequent `release-pr` commands, potentially generating incorrect
207+
// changelogs that include the entire commit history. Setting `force-tag-creation` to
208+
// true ensures the tag is created immediately, allowing release-please to
209+
// correctly identify the previous release.
210+
// Absence defaults to false.
211+
"force-tag-creation": true
201212

202213
// Skip creating GitHub Releases
203214
// Absence defaults to false and Releases will be created. Release-Please still

schemas/config.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@
6666
"description": "Create the GitHub release in draft mode. Defaults to `false`.",
6767
"type": "boolean"
6868
},
69+
"force-tag-creation": {
70+
"description": "Force the creation of a Git tag for the release. This is particularly useful when `draft` is enabled, because GitHub does not create a Git tag for draft releases until they are published. This 'lazy tag creation' causes release-please to fail to find the previous release, potentially generating incorrect changelogs. Setting this to `true` ensures the tag is created immediately. Defaults to `false`.",
71+
"type": "boolean"
72+
},
6973
"prerelease": {
7074
"description": "Create the GitHub release as prerelease. Defaults to `false`.",
7175
"type": "boolean"
@@ -468,6 +472,7 @@
468472
"skip-github-release": true,
469473
"skip-changelog": true,
470474
"draft": true,
475+
"force-tag-creation": true,
471476
"prerelease": true,
472477
"draft-pull-request": true,
473478
"label": true,

src/bin/release-please.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ interface ManifestConfigArgs {
8484

8585
interface ReleaseArgs {
8686
draft?: boolean;
87+
forceTag?: boolean;
8788
prerelease?: boolean;
8889
releaseLabel?: string;
8990
snapshotLabel?: string;
@@ -206,6 +207,11 @@ function releaseOptions(yargs: yargs.Argv): yargs.Argv {
206207
type: 'boolean',
207208
default: false,
208209
})
210+
.option('force-tag-creation', {
211+
describe: 'Force the creation of a Git tag for the release.',
212+
type: 'boolean',
213+
default: false,
214+
})
209215
.option('prerelease', {
210216
describe:
211217
'mark release that have prerelease versions ' +
@@ -568,6 +574,7 @@ const createReleaseCommand: yargs.CommandModule<{}, CreateReleaseArgs> = {
568574
component: argv.component,
569575
packageName: argv.packageName,
570576
draft: argv.draft,
577+
forceTag: argv.forceTag,
571578
prerelease: argv.prerelease,
572579
includeComponentInTag: argv.monorepoTags,
573580
includeVInTag: argv.includeVInTags,
@@ -731,6 +738,7 @@ const bootstrapCommand: yargs.CommandModule<{}, BootstrapArgs> = {
731738
component: argv.component,
732739
packageName: argv.packageName,
733740
draft: argv.draft,
741+
forceTag: argv.forceTag,
734742
prerelease: argv.prerelease,
735743
draftPullRequest: argv.draftPullRequest,
736744
bumpMinorPreMajor: argv.bumpMinorPreMajor,

src/github.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ interface TagIteratorOptions {
173173
export interface ReleaseOptions {
174174
draft?: boolean;
175175
prerelease?: boolean;
176+
forceTag?: boolean;
176177
}
177178

178179
export interface GitHubRelease {
@@ -1391,6 +1392,25 @@ export class GitHub {
13911392
release: Release,
13921393
options: ReleaseOptions = {}
13931394
): Promise<GitHubRelease> => {
1395+
if (options.forceTag) {
1396+
try {
1397+
await this.octokit.git.createRef({
1398+
owner: this.repository.owner,
1399+
repo: this.repository.repo,
1400+
ref: `refs/tags/${release.tag.toString()}`,
1401+
sha: release.sha,
1402+
});
1403+
} catch (err) {
1404+
// ignore if tag already exists
1405+
if ((err as RequestError).status === 422) {
1406+
this.logger.debug(
1407+
`Tag ${release.tag.toString()} already exists, skipping tag creation`
1408+
);
1409+
} else {
1410+
throw err;
1411+
}
1412+
}
1413+
}
13941414
const resp = await this.octokit.repos.createRelease({
13951415
name: release.name,
13961416
owner: this.repository.owner,

src/manifest.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export interface ReleaserConfig {
107107
skipGithubRelease?: boolean; // Note this should be renamed to skipGitHubRelease in next major release\
108108
skipChangelog?: boolean;
109109
draft?: boolean;
110+
forceTag?: boolean;
110111
prerelease?: boolean;
111112
draftPullRequest?: boolean;
112113
component?: string;
@@ -152,6 +153,7 @@ export interface CandidateRelease extends Release {
152153
pullRequest: PullRequest;
153154
path: string;
154155
draft?: boolean;
156+
forceTag?: boolean;
155157
prerelease?: boolean;
156158
}
157159

@@ -166,6 +168,7 @@ interface ReleaserConfigJson {
166168
'skip-github-release'?: boolean;
167169
'skip-changelog'?: boolean;
168170
draft?: boolean;
171+
'force-tag-creation'?: boolean;
169172
prerelease?: boolean;
170173
'draft-pull-request'?: boolean;
171174
label?: string;
@@ -1186,6 +1189,7 @@ export class Manifest {
11861189
path,
11871190
pullRequest,
11881191
draft: config.draft ?? this.draft,
1192+
forceTag: config.forceTag,
11891193
prerelease:
11901194
config.prerelease &&
11911195
(!!release.tag.version.preRelease ||
@@ -1313,6 +1317,7 @@ export class Manifest {
13131317
const githubRelease = await this.github.createRelease(release, {
13141318
draft: release.draft,
13151319
prerelease: release.prerelease,
1320+
forceTag: release.forceTag,
13161321
});
13171322

13181323
return {
@@ -1387,6 +1392,7 @@ function extractReleaserConfig(
13871392
skipGithubRelease: config['skip-github-release'],
13881393
skipChangelog: config['skip-changelog'],
13891394
draft: config.draft,
1395+
forceTag: config['force-tag-creation'],
13901396
prerelease: config.prerelease,
13911397
draftPullRequest: config['draft-pull-request'],
13921398
component: config['component'],
@@ -1743,6 +1749,7 @@ function mergeReleaserConfig(
17431749
pathConfig.skipGithubRelease ?? defaultConfig.skipGithubRelease,
17441750
skipChangelog: pathConfig.skipChangelog ?? defaultConfig.skipChangelog,
17451751
draft: pathConfig.draft ?? defaultConfig.draft,
1752+
forceTag: pathConfig.forceTag ?? defaultConfig.forceTag,
17461753
draftPullRequest:
17471754
pathConfig.draftPullRequest ?? defaultConfig.draftPullRequest,
17481755
prerelease: pathConfig.prerelease ?? defaultConfig.prerelease,

test/github.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,85 @@ describe('GitHub', () => {
831831
expect(release.draft).to.be.true;
832832
});
833833

834+
it('should create a draft release with forced tag', async () => {
835+
req
836+
.post('/repos/fake/fake/git/refs', body => {
837+
expect(body.ref).to.eql('refs/tags/v1.2.3');
838+
expect(body.sha).to.eql('abc123');
839+
return true;
840+
})
841+
.reply(201, {
842+
ref: 'refs/tags/v1.2.3',
843+
object: {
844+
sha: 'abc123',
845+
},
846+
});
847+
req
848+
.post('/repos/fake/fake/releases', body => {
849+
snapshot(body);
850+
return true;
851+
})
852+
.reply(200, {
853+
tag_name: 'v1.2.3',
854+
draft: true,
855+
html_url: 'https://github.com/fake/fake/releases/v1.2.3',
856+
upload_url:
857+
'https://uploads.github.com/repos/fake/fake/releases/1/assets{?name,label}',
858+
target_commitish: 'abc123',
859+
});
860+
const release = await github.createRelease(
861+
{
862+
tag: new TagName(Version.parse('1.2.3')),
863+
sha: 'abc123',
864+
notes: 'Some release notes',
865+
},
866+
{draft: true, forceTag: true}
867+
);
868+
req.done();
869+
expect(release).to.not.be.undefined;
870+
expect(release.tagName).to.eql('v1.2.3');
871+
expect(release.sha).to.eql('abc123');
872+
expect(release.draft).to.be.true;
873+
});
874+
875+
it('should create a draft release with forced tag (already exists)', async () => {
876+
req
877+
.post('/repos/fake/fake/git/refs', body => {
878+
expect(body.ref).to.eql('refs/tags/v1.2.3');
879+
expect(body.sha).to.eql('abc123');
880+
return true;
881+
})
882+
.reply(422, {
883+
message: 'Reference already exists',
884+
});
885+
req
886+
.post('/repos/fake/fake/releases', body => {
887+
snapshot(body);
888+
return true;
889+
})
890+
.reply(200, {
891+
tag_name: 'v1.2.3',
892+
draft: true,
893+
html_url: 'https://github.com/fake/fake/releases/v1.2.3',
894+
upload_url:
895+
'https://uploads.github.com/repos/fake/fake/releases/1/assets{?name,label}',
896+
target_commitish: 'abc123',
897+
});
898+
const release = await github.createRelease(
899+
{
900+
tag: new TagName(Version.parse('1.2.3')),
901+
sha: 'abc123',
902+
notes: 'Some release notes',
903+
},
904+
{draft: true, forceTag: true}
905+
);
906+
req.done();
907+
expect(release).to.not.be.undefined;
908+
expect(release.tagName).to.eql('v1.2.3');
909+
expect(release.sha).to.eql('abc123');
910+
expect(release.draft).to.be.true;
911+
});
912+
834913
it('should create a prerelease release', async () => {
835914
req
836915
.post('/repos/fake/fake/releases', body => {

0 commit comments

Comments
 (0)