Skip to content

Commit 4178e5a

Browse files
feat: 'commits-since' configuration option (#1451)
* Add 'commits-since' configuration option for release drafts When the last release does not exist for a specific project or branch, all commits from the repository are searched. For a repository with a long history, it can block generating release notes due to timeout or due to too long release notes. * try to validate input * feat: require ISO 8601 date for commits-since * test: test the actual string in commits-since valid config * refactor: rename commits-since to initial-commits-since * chore: debug logs instead of info for validation * style: run linter * chore: update build * chore: upadte build * docs: update README --------- Co-authored-by: Clément Chanchevrier <[email protected]> Co-authored-by: Clément Chanchevrier <[email protected]>
1 parent 686295d commit 4178e5a

9 files changed

Lines changed: 194 additions & 14 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ You can configure Release Drafter using the following key in your `.github/relea
146146
| `commitish` | Optional | The release target, i.e. branch or commit it should point to. Default: the ref that release-drafter runs for, e.g. `refs/heads/master` if configured to run on pushes to `master`. |
147147
| `filter-by-commitish` | Optional | Filter previous releases to consider only those with the target matching `commitish`. Default: `false`. |
148148
| `include-paths` | Optional | Restrict pull requests included in the release notes to only the pull requests that modified any of the paths in this array. Supports files and directories. Default: `[]` |
149+
| `initial-commits-since` | Optional | When drafting your first release, limit the amount of scanned commits. Expects an ISO 8601 date, ex: `"2025-06-18T10:29:51Z"`. Default: `""` (unlimited) |
149150

150151
Release Drafter also supports [Probot Config](https://github.com/probot/probot-config), if you want to store your configuration files in a central repository. This allows you to share configurations between projects, and create a organization-wide configuration file by creating a repository named `.github` with the file `.github/release-drafter.yml`.
151152

@@ -374,6 +375,7 @@ The Release Drafter GitHub Action accepts a number of optional inputs directly i
374375
| `commitish` | A string specifying the target branch for the release being created. |
375376
| `header` | A string that would be added before the template body. |
376377
| `footer` | A string that would be added after the template body. |
378+
| `initial-commits-since` | When drafting your first release, limit the amount of scanned commits. Expects an ISO 8601 date, ex: `"2025-06-18T10:29:51Z"`. Default: `""` (unlimited) |
377379
| `disable-releaser` | A boolean indicating whether the releaser mode is disabled. |
378380
| `disable-autolabeler` | A boolean indicating whether the autolabeler mode is disabled. |
379381

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ inputs:
7373
A boolean indicating whether the autolabeler mode is disabled.
7474
required: false
7575
default: ''
76+
initial-commits-since:
77+
description: |
78+
A date in ISO format (eg: '2025-06-18T10:29:51Z') marking the initial commit for the release draft. This is applied only when no prior release is available.
79+
required: false
7680
outputs:
7781
id:
7882
description: The ID of the release that was created or updated.

dist/index.js

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193646,6 +193646,7 @@ function getInput() {
193646193646
: undefined,
193647193647
preReleaseIdentifier: core.getInput('prerelease-identifier') || undefined,
193648193648
latest: core.getInput('latest')?.toLowerCase() || undefined,
193649+
commitsSince: core.getInput('initial-commits-since') || undefined,
193649193650
}
193650193651
}
193651193652

@@ -193677,6 +193678,10 @@ function updateConfigFromInput(config, input) {
193677193678
config.latest = config.prerelease
193678193679
? 'false'
193679193680
: input.latest || config.latest || undefined
193681+
193682+
if (input.commitsSince) {
193683+
config['initial-commits-since'] = input.commitsSince
193684+
}
193680193685
}
193681193686

193682193687
function setActionOutput(
@@ -193714,6 +193719,8 @@ function setActionOutput(
193714193719
const _ = __nccwpck_require__(90250)
193715193720
const { log } = __nccwpck_require__(71911)
193716193721
const { paginate } = __nccwpck_require__(46418)
193722+
const Joi = __nccwpck_require__(20918)
193723+
const core = __nccwpck_require__(42186)
193717193724

193718193725
const findCommitsWithPathChangesQuery = /* GraphQL */ `
193719193726
query findCommitsWithPathChangesQuery(
@@ -193833,15 +193840,32 @@ const findCommitsWithAssociatedPullRequests = async ({
193833193840
allCommits,
193834193841
includedIds = {}
193835193842

193843+
const since = lastRelease
193844+
? lastRelease.created_at
193845+
: config['initial-commits-since']
193846+
const validationResult = Joi.date().iso().validate(since)
193847+
193848+
core.debug(' since value: ' + since)
193849+
core.debug(
193850+
' since value validation.value: ' +
193851+
validationResult.value +
193852+
' error: ' +
193853+
validationResult.error
193854+
)
193855+
193856+
// The validation result contains either an error or the validated value
193857+
if (validationResult.error) {
193858+
core.setFailed(validationResult.error.message)
193859+
throw new Error(validationResult.error.message)
193860+
}
193861+
193836193862
if (includePaths.length > 0) {
193837193863
var anyChanges = false
193838193864
for (const path of includePaths) {
193839193865
const pathData = await paginate(
193840193866
context.octokit.graphql,
193841193867
findCommitsWithPathChangesQuery,
193842-
lastRelease
193843-
? { ...variables, since: lastRelease.created_at, path }
193844-
: { ...variables, path },
193868+
{ ...variables, since: since, path },
193845193869
dataPath
193846193870
)
193847193871
const commitsWithPathChanges = _.get(pathData, [...dataPath, 'nodes'])
@@ -193859,22 +193883,22 @@ const findCommitsWithAssociatedPullRequests = async ({
193859193883
}
193860193884
}
193861193885

193862-
if (lastRelease) {
193886+
if (since) {
193863193887
log({
193864193888
context,
193865-
message: `Fetching parent commits of ${targetCommitish} since ${lastRelease.created_at}`,
193889+
message: `Fetching parent commits of ${targetCommitish} since ${since}`,
193866193890
})
193867193891

193868193892
data = await paginate(
193869193893
context.octokit.graphql,
193870193894
findCommitsWithAssociatedPullRequestsQuery,
193871-
{ ...variables, since: lastRelease.created_at },
193895+
{ ...variables, since: since },
193872193896
dataPath
193873193897
)
193874193898
// GraphQL call is inclusive of commits from the specified dates. This means the final
193875193899
// commit from the last tag is included, so we remove this here.
193876193900
allCommits = _.get(data, [...dataPath, 'nodes']).filter(
193877-
(commit) => commit.committedDate != lastRelease.created_at
193901+
(commit) => commit.committedDate != since
193878193902
)
193879193903
} else {
193880193904
log({ context, message: `Fetching parent commits of ${targetCommitish}` })
@@ -194735,6 +194759,8 @@ const schema = (context) => {
194735194759
DEFAULT_CONFIG['filter-by-commitish']
194736194760
),
194737194761

194762+
'initial-commits-since': Joi.date().iso().allow(''),
194763+
194738194764
'include-pre-releases': Joi.boolean().default(
194739194765
DEFAULT_CONFIG['include-pre-releases']
194740194766
),

index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ function getInput() {
253253
: undefined,
254254
preReleaseIdentifier: core.getInput('prerelease-identifier') || undefined,
255255
latest: core.getInput('latest')?.toLowerCase() || undefined,
256+
commitsSince: core.getInput('initial-commits-since') || undefined,
256257
}
257258
}
258259

@@ -284,6 +285,10 @@ function updateConfigFromInput(config, input) {
284285
config.latest = config.prerelease
285286
? 'false'
286287
: input.latest || config.latest || undefined
288+
289+
if (input.commitsSince) {
290+
config['initial-commits-since'] = input.commitsSince
291+
}
287292
}
288293

289294
function setActionOutput(

lib/commits.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
const _ = require('lodash')
22
const { log } = require('./log')
33
const { paginate } = require('./pagination')
4+
const Joi = require('joi')
5+
const core = require('@actions/core')
46

57
const findCommitsWithPathChangesQuery = /* GraphQL */ `
68
query findCommitsWithPathChangesQuery(
@@ -120,15 +122,32 @@ const findCommitsWithAssociatedPullRequests = async ({
120122
allCommits,
121123
includedIds = {}
122124

125+
const since = lastRelease
126+
? lastRelease.created_at
127+
: config['initial-commits-since']
128+
const validationResult = Joi.date().iso().validate(since)
129+
130+
core.debug(' since value: ' + since)
131+
core.debug(
132+
' since value validation.value: ' +
133+
validationResult.value +
134+
' error: ' +
135+
validationResult.error
136+
)
137+
138+
// The validation result contains either an error or the validated value
139+
if (validationResult.error) {
140+
core.setFailed(validationResult.error.message)
141+
throw new Error(validationResult.error.message)
142+
}
143+
123144
if (includePaths.length > 0) {
124145
var anyChanges = false
125146
for (const path of includePaths) {
126147
const pathData = await paginate(
127148
context.octokit.graphql,
128149
findCommitsWithPathChangesQuery,
129-
lastRelease
130-
? { ...variables, since: lastRelease.created_at, path }
131-
: { ...variables, path },
150+
{ ...variables, since: since, path },
132151
dataPath
133152
)
134153
const commitsWithPathChanges = _.get(pathData, [...dataPath, 'nodes'])
@@ -146,22 +165,22 @@ const findCommitsWithAssociatedPullRequests = async ({
146165
}
147166
}
148167

149-
if (lastRelease) {
168+
if (since) {
150169
log({
151170
context,
152-
message: `Fetching parent commits of ${targetCommitish} since ${lastRelease.created_at}`,
171+
message: `Fetching parent commits of ${targetCommitish} since ${since}`,
153172
})
154173

155174
data = await paginate(
156175
context.octokit.graphql,
157176
findCommitsWithAssociatedPullRequestsQuery,
158-
{ ...variables, since: lastRelease.created_at },
177+
{ ...variables, since: since },
159178
dataPath
160179
)
161180
// GraphQL call is inclusive of commits from the specified dates. This means the final
162181
// commit from the last tag is included, so we remove this here.
163182
allCommits = _.get(data, [...dataPath, 'nodes']).filter(
164-
(commit) => commit.committedDate != lastRelease.created_at
183+
(commit) => commit.committedDate != since
165184
)
166185
} else {
167186
log({ context, message: `Fetching parent commits of ${targetCommitish}` })

lib/schema.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ const schema = (context) => {
8989
DEFAULT_CONFIG['filter-by-commitish']
9090
),
9191

92+
'initial-commits-since': Joi.date().iso().allow(''),
93+
9294
'include-pre-releases': Joi.boolean().default(
9395
DEFAULT_CONFIG['include-pre-releases']
9496
),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
initial-commits-since: '2025-06-18T10:29:51Z'
2+
template: 'dummy'

test/index.test.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3454,4 +3454,111 @@ describe('release-drafter', () => {
34543454
})
34553455
})
34563456
})
3457+
3458+
describe('with initial-commits-since', () => {
3459+
it('use commits since from last release', async () => {
3460+
getConfigMock('config-with-commits-since.yml')
3461+
3462+
nock('https://api.github.com')
3463+
.post('/graphql', (body) => {
3464+
if (
3465+
body.query.includes('query findCommitsWithAssociatedPullRequests')
3466+
) {
3467+
expect(body.variables.since).toBe('2018-06-29T05:45:15Z')
3468+
return true
3469+
}
3470+
return false
3471+
})
3472+
.reply(200, graphqlCommitsNoPRsPayload)
3473+
3474+
nock('https://api.github.com')
3475+
.get(
3476+
'/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
3477+
)
3478+
.reply(200, [releasePayload])
3479+
3480+
nock('https://api.github.com')
3481+
.post('/repos/toolmantim/release-drafter-test-project/releases')
3482+
.reply(200, releasePayload)
3483+
3484+
const payload = pushPayload
3485+
3486+
await probot.receive({
3487+
name: 'push',
3488+
payload,
3489+
})
3490+
3491+
expect.assertions(1)
3492+
})
3493+
3494+
it('use commits since from config', async () => {
3495+
getConfigMock('config-with-commits-since.yml')
3496+
3497+
nock('https://api.github.com')
3498+
.post('/graphql', (body) => {
3499+
if (
3500+
body.query.includes('query findCommitsWithAssociatedPullRequests')
3501+
) {
3502+
expect(body.variables.since).toBe('2025-06-18T10:29:51.000Z')
3503+
return true
3504+
}
3505+
return false
3506+
})
3507+
.reply(200, graphqlCommitsNoPRsPayload)
3508+
3509+
nock('https://api.github.com')
3510+
.get(
3511+
'/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
3512+
)
3513+
.reply(200, [])
3514+
3515+
nock('https://api.github.com')
3516+
.post('/repos/toolmantim/release-drafter-test-project/releases')
3517+
.reply(200, releasePayload)
3518+
3519+
const payload = pushPayload
3520+
3521+
await probot.receive({
3522+
name: 'push',
3523+
payload,
3524+
})
3525+
3526+
expect.assertions(1)
3527+
})
3528+
3529+
it('use empty commit since', async () => {
3530+
getConfigMock('config.yml')
3531+
3532+
nock('https://api.github.com')
3533+
.post('/graphql', (body) => {
3534+
if (
3535+
body.query.includes('query findCommitsWithAssociatedPullRequests')
3536+
) {
3537+
expect(body.variables.since).toBeUndefined()
3538+
return true
3539+
}
3540+
return false
3541+
})
3542+
.reply(200, graphqlCommitsNoPRsPayload)
3543+
3544+
nock('https://api.github.com')
3545+
.get(
3546+
'/repos/toolmantim/release-drafter-test-project/releases?per_page=100'
3547+
)
3548+
.reply(200, [])
3549+
3550+
nock('https://api.github.com')
3551+
.post('/repos/toolmantim/release-drafter-test-project/releases')
3552+
.reply(200, releasePayload)
3553+
3554+
const payload = pushPayload
3555+
3556+
await probot.receive({
3557+
name: 'push',
3558+
payload,
3559+
})
3560+
3561+
expect.assertions(1)
3562+
})
3563+
})
34573564
})

test/schema.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ const validConfigs = [
2121
[{ template, footer: 'I am on bottm' }],
2222
[{ template, header: 'I am on top', footer: 'I am on bottm' }],
2323
[{ template, 'pull-request-limit': 49 }],
24+
[
25+
{ template, 'initial-commits-since': '2025-06-18T10:29:51Z' },
26+
{ template, 'initial-commits-since': new Date('2025-06-18T10:29:51Z') },
27+
],
28+
[{ template, 'initial-commits-since': '' }],
2429
]
2530

2631
const invalidConfigs = [
@@ -55,6 +60,14 @@ const invalidConfigs = [
5560
[{ replacers: [{ search: '123', replace: 123 }] }, 'must be a string'],
5661
[{ commitish: false }, 'must be a string'],
5762
[{ 'pull-request-limit': 'forty nine' }, 'must be a number'],
63+
[{ 'initial-commits-since': 'a day' }, 'must be in ISO 8601 date format'],
64+
[
65+
{
66+
'initial-commits-since':
67+
'Wed Dec 10 2025 19:33:48 GMT+0100 (Central European Standard Time)',
68+
},
69+
'must be in ISO 8601 date format',
70+
],
5871
]
5972

6073
describe('schema', () => {

0 commit comments

Comments
 (0)