Skip to content

Commit 7f8a68f

Browse files
authored
refactor: rewrite changelog generator to use commit ranges instead of milestones (#631)
Replace the milestone-based changelog generator with one that walks commit ancestry between two git refs, resolves commits to merged PRs, parses conventional commit prefixes from PR titles, and matches published GHSAs whose fix commits fall in the range. Key changes: - GitHubApi: add commitsBetweenRefs, prsForCommits, publishedAdvisories - ChangelogGenerator: full rewrite with CC parsing, GHSA matching, area label sub-grouping (h4), and configurable heading depth - CLI: replace --milestone with --base/--head/--title/--no-ghsa - changelog-pr: replace --milestone with --version - Workflows: add base_ref/version inputs, run-name with dry-run status, make milestone optional (still used for preflight checks) - Taskfile: use empty-string convention for boolean vars
1 parent f158c24 commit 7f8a68f

7 files changed

Lines changed: 420 additions & 111 deletions

File tree

.github/workflows/build-patch.yml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: Build Patch Release
2+
run-name: "Build Patch Release ${{ inputs.version }}${{ inputs.dry_run && ' (dry run)' || '' }}"
23

34
on:
45
workflow_dispatch:
@@ -15,10 +16,14 @@ on:
1516
description: Patch zip filename (e.g., 8-0-0-Patch-1.zip)
1617
required: true
1718
type: string
18-
milestone:
19-
description: GitHub milestone name for changelog (e.g., 8.0.0.1)
19+
version:
20+
description: Human-readable version (e.g., 8.0.0.1)
2021
required: true
2122
type: string
23+
milestone:
24+
description: GitHub milestone name for preflight check (e.g., 8.0.0.1)
25+
required: false
26+
type: string
2227
patch_number:
2328
description: Patch number to set in version.php $v_realpatch (e.g., 1)
2429
required: true
@@ -117,15 +122,15 @@ jobs:
117122
working-directory: devops-tools
118123

119124
- name: Preflight checks
120-
if: '!inputs.skip_milestone_check || !inputs.skip_ghsa_check'
125+
if: '(!inputs.skip_milestone_check && inputs.milestone != '''') || !inputs.skip_ghsa_check'
121126
working-directory: devops-tools
122127
env:
123128
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
124129
run: >-
125130
task release:preflight
126131
MILESTONE='${{ inputs.milestone }}'
127-
SKIP_MILESTONE='${{ inputs.skip_milestone_check }}'
128-
SKIP_GHSA='${{ inputs.skip_ghsa_check }}'
132+
SKIP_MILESTONE='${{ (inputs.skip_milestone_check || inputs.milestone == '') && '1' || '' }}'
133+
SKIP_GHSA='${{ inputs.skip_ghsa_check && '1' || '' }}'
129134
130135
- name: Bump version.php
131136
working-directory: devops-tools
@@ -151,7 +156,9 @@ jobs:
151156
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
152157
run: >-
153158
task release:changelog
154-
MILESTONE='${{ inputs.milestone }}'
159+
BASE_REF='${{ inputs.version_start }}'
160+
HEAD_REF='${{ inputs.version_branch }}'
161+
TITLE='${{ inputs.version }}'
155162
156163
- name: Build theme styles
157164
if: inputs.copy_styles
@@ -253,6 +260,6 @@ jobs:
253260
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
254261
run: >-
255262
task release:changelog-pr
256-
MILESTONE='${{ inputs.milestone }}'
263+
VERSION='${{ inputs.version }}'
257264
BRANCHES='${{ inputs.version_branch }},master'
258265
OPENEMR_DIR='${{ env.OPENEMR_DIR }}'

.github/workflows/build-release.yml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: Build Release
2+
run-name: "Build Release ${{ inputs.version }}${{ inputs.dry_run && ' (dry run)' || '' }}"
23

34
on:
45
workflow_dispatch:
@@ -15,10 +16,14 @@ on:
1516
description: Git tag (e.g., v8_0_0). Required unless dry run.
1617
required: false
1718
type: string
18-
milestone:
19-
description: GitHub milestone name (e.g., 8.0.0)
19+
base_ref:
20+
description: Previous release tag for changelog (e.g., v7_3_7)
2021
required: true
2122
type: string
23+
milestone:
24+
description: GitHub milestone name for preflight check (e.g., 8.0.0)
25+
required: false
26+
type: string
2227
dry_run:
2328
description: Dry run — build only, skip release
2429
type: boolean
@@ -113,15 +118,15 @@ jobs:
113118
run: task release:setup
114119

115120
- name: Preflight checks
116-
if: '!inputs.skip_milestone_check || !inputs.skip_ghsa_check'
121+
if: '(!inputs.skip_milestone_check && inputs.milestone != '''') || !inputs.skip_ghsa_check'
117122
working-directory: devops-tools
118123
env:
119124
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
120125
run: >-
121126
task release:preflight
122127
MILESTONE='${{ inputs.milestone }}'
123-
SKIP_MILESTONE='${{ inputs.skip_milestone_check }}'
124-
SKIP_GHSA='${{ inputs.skip_ghsa_check }}'
128+
SKIP_MILESTONE='${{ (inputs.skip_milestone_check || inputs.milestone == '') && '1' || '' }}'
129+
SKIP_GHSA='${{ inputs.skip_ghsa_check && '1' || '' }}'
125130
126131
- name: Version bump (full mode)
127132
working-directory: devops-tools
@@ -146,7 +151,9 @@ jobs:
146151
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
147152
run: >-
148153
task release:changelog
149-
MILESTONE='${{ inputs.milestone }}'
154+
BASE_REF='${{ inputs.base_ref }}'
155+
HEAD_REF='${{ inputs.version_branch }}'
156+
TITLE='${{ inputs.version }}'
150157
151158
- name: Create annotated tag and GitHub release
152159
if: '!inputs.dry_run'
@@ -240,7 +247,7 @@ jobs:
240247
GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
241248
run: >-
242249
task release:changelog-pr
243-
MILESTONE='${{ inputs.milestone }}'
250+
VERSION='${{ inputs.version }}'
244251
BRANCHES='${{ inputs.version_branch }},master'
245252
OPENEMR_DIR='${{ env.OPENEMR_DIR }}'
246253

tools/release/Taskfile.yml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ tasks:
1616
- vendor/autoload.php
1717

1818
changelog:
19-
desc: Generate changelog from GitHub milestone
19+
desc: Generate changelog from commit range
2020
requires:
21-
vars: [MILESTONE]
21+
vars: [BASE_REF]
2222
deps: [setup]
2323
cmds:
24-
- mkdir -p {{.OUTPUT_DIR}}
24+
- mkdir -p {{shellQuote .OUTPUT_DIR}}
2525
- >-
2626
php bin/changelog.php
27-
--milestone={{shellQuote .MILESTONE}}
27+
--base={{shellQuote .BASE_REF}}
28+
{{if .HEAD_REF}}--head={{shellQuote .HEAD_REF}}{{end}}
29+
{{if .TITLE}}--title={{shellQuote .TITLE}}{{end}}
2830
--repo={{shellQuote .REPO}}
2931
--output={{shellQuote .OUTPUT_DIR}}/changelog.md
32+
{{if eq .NO_GHSA "1"}}--no-ghsa{{end}}
3033
3134
version-bump:
3235
desc: Bump version numbers in OpenEMR source files
@@ -43,16 +46,14 @@ tasks:
4346
4447
preflight:
4548
desc: Run pre-release checks
46-
requires:
47-
vars: [MILESTONE]
4849
deps: [setup]
4950
cmds:
5051
- >-
5152
php bin/preflight.php
52-
--milestone={{shellQuote .MILESTONE}}
53+
{{if and .MILESTONE (ne .SKIP_MILESTONE "1")}}--milestone={{shellQuote .MILESTONE}}{{end}}
5354
--repo={{shellQuote .REPO}}
54-
{{if .SKIP_MILESTONE}}--skip-milestone{{end}}
55-
{{if .SKIP_GHSA}}--skip-ghsa{{end}}
55+
{{if eq .SKIP_MILESTONE "1"}}--skip-milestone{{end}}
56+
{{if eq .SKIP_GHSA "1"}}--skip-ghsa{{end}}
5657
5758
checksum:
5859
desc: Generate checksum sidecar files
@@ -100,12 +101,12 @@ tasks:
100101
changelog-pr:
101102
desc: Create CHANGELOG.md update PRs
102103
requires:
103-
vars: [MILESTONE, BRANCHES, OPENEMR_DIR]
104+
vars: [VERSION, BRANCHES, OPENEMR_DIR]
104105
deps: [setup]
105106
cmds:
106107
- >-
107108
php bin/changelog-pr.php
108-
--milestone={{shellQuote .MILESTONE}}
109+
--version={{shellQuote .VERSION}}
109110
--branches={{shellQuote .BRANCHES}}
110111
--openemr-dir={{shellQuote .OPENEMR_DIR}}
111112
--changelog-file={{shellQuote .OUTPUT_DIR}}/changelog.md

tools/release/bin/changelog-pr.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
(new SingleCommandApplication())
2525
->setName('changelog-pr')
2626
->setDescription('Create CHANGELOG.md update PRs')
27-
->addOption('milestone', 'm', InputOption::VALUE_REQUIRED, 'Milestone name')
27+
->addOption('version', null, InputOption::VALUE_REQUIRED, 'Version string (e.g., 8.0.0.3)')
2828
->addOption('branches', null, InputOption::VALUE_REQUIRED, 'Comma-separated target branches')
2929
->addOption('openemr-dir', null, InputOption::VALUE_REQUIRED, 'Path to openemr checkout')
3030
->addOption('changelog-file', null, InputOption::VALUE_REQUIRED, 'Path to changelog entry file')
3131
->addOption('repo', 'r', InputOption::VALUE_REQUIRED, 'GitHub repo', 'openemr/openemr')
3232
->setCode(function (InputInterface $input, OutputInterface $output): int {
33-
/** @var string $milestone */
34-
$milestone = $input->getOption('milestone');
33+
/** @var string $version */
34+
$version = $input->getOption('version');
3535
/** @var string $openemrDir */
3636
$openemrDir = $input->getOption('openemr-dir');
3737
/** @var string $changelogFile */
@@ -41,7 +41,7 @@
4141
/** @var string $branchesRaw */
4242
$branchesRaw = $input->getOption('branches');
4343

44-
foreach (['milestone', 'branches', 'openemr-dir', 'changelog-file'] as $required) {
44+
foreach (['version', 'branches', 'openemr-dir', 'changelog-file'] as $required) {
4545
if ($input->getOption($required) === null) {
4646
$output->writeln("<error>--{$required} is required</error>");
4747
return 1;
@@ -63,7 +63,7 @@
6363

6464
foreach ($branches as $branch) {
6565
$branch = trim($branch);
66-
$prBranch = "changelog-{$milestone}-{$branch}";
66+
$prBranch = "changelog-{$version}-{$branch}";
6767

6868
// Check if PR already exists
6969
$check = new Process(
@@ -97,7 +97,7 @@
9797
// Commit, push, create PR
9898
(new Process(['git', 'add', 'CHANGELOG.md'], $openemrDir))->mustRun();
9999
(new Process(
100-
['git', 'commit', '-m', "docs: add {$milestone} changelog"],
100+
['git', 'commit', '-m', "docs: add {$version} changelog"],
101101
$openemrDir,
102102
))->mustRun();
103103
(new Process(['git', 'push', 'origin', $prBranch], $openemrDir))->mustRun();
@@ -106,8 +106,8 @@
106106
'--repo', $repo,
107107
'--base', $branch,
108108
'--head', $prBranch,
109-
'--title', "docs: add {$milestone} changelog",
110-
'--body', "Add changelog entry for {$milestone} release.",
109+
'--title', "docs: add {$version} changelog",
110+
'--body', "Add changelog entry for {$version} release.",
111111
]))->mustRun();
112112

113113
$output->writeln("Created PR for <info>{$prBranch}</info> → {$branch}");

tools/release/bin/changelog.php

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<?php
33

44
/**
5-
* Generate a changelog from a GitHub milestone.
5+
* Generate a changelog from the commit range between two git refs.
66
*
77
* @package openemr-devops
88
* @link https://www.open-emr.org
@@ -24,35 +24,38 @@
2424

2525
(new SingleCommandApplication())
2626
->setName('changelog')
27-
->setDescription('Generate changelog from a GitHub milestone')
28-
->addOption('milestone', 'm', InputOption::VALUE_REQUIRED, 'Milestone name')
27+
->setDescription('Generate changelog from a commit range')
28+
->addOption('base', 'b', InputOption::VALUE_REQUIRED, 'Base ref (tag)')
29+
->addOption('head', null, InputOption::VALUE_REQUIRED, 'Head ref (tag or branch)', 'HEAD')
30+
->addOption('title', 't', InputOption::VALUE_REQUIRED, 'Version string for the heading')
31+
->addOption('no-ghsa', null, InputOption::VALUE_NONE, 'Disable security advisories section')
2932
->addOption('repo', 'r', InputOption::VALUE_REQUIRED, 'GitHub repo (owner/name)', 'openemr/openemr')
3033
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Output file path (omit for stdout)')
3134
->setCode(function (InputInterface $input, OutputInterface $output): int {
32-
/** @var ?string $milestone */
33-
$milestone = $input->getOption('milestone');
34-
if ($milestone === null) {
35-
$output->writeln('<error>--milestone is required</error>');
35+
/** @var ?string $base */
36+
$base = $input->getOption('base');
37+
if ($base === null) {
38+
$output->writeln('<error>--base is required</error>');
3639
return 1;
3740
}
3841

42+
/** @var string $head */
43+
$head = $input->getOption('head');
44+
/** @var ?string $title */
45+
$title = $input->getOption('title');
46+
$includeGhsa = $input->getOption('no-ghsa') !== true;
47+
3948
/** @var string $repo */
4049
$repo = $input->getOption('repo');
4150
$api = new GitHubApi($repo);
4251

43-
$milestoneNumber = $api->findMilestone($milestone);
44-
$output->writeln(
45-
"Found milestone <info>{$milestone}</info> (#{$milestoneNumber})",
46-
OutputInterface::VERBOSITY_VERBOSE,
47-
);
48-
49-
$issues = $api->closedIssuesForMilestone($milestoneNumber);
5052
$output->writeln(
51-
sprintf('Fetched <info>%d</info> closed issues', count($issues)),
53+
"Generating changelog for <info>{$base}...{$head}</info>",
5254
OutputInterface::VERBOSITY_VERBOSE,
5355
);
5456

55-
$changelog = (new ChangelogGenerator())->generate($milestone, $milestoneNumber, $repo, $issues);
57+
$generator = new ChangelogGenerator($api, $repo);
58+
$changelog = $generator->generate($base, $head, $title, $includeGhsa);
5659

5760
/** @var ?string $outputFile */
5861
$outputFile = $input->getOption('output');

0 commit comments

Comments
 (0)