Skip to content

Commit 603502b

Browse files
BigSamugithub-actions[bot]JohnathonBowersCMDWillYangMadaniKK
committed
[OSCI][FEAT] Changelog Project - PoC Changelog and release notes automation tool - OpenSearch Dashboards (opensearch-project#5519)
Refactor and Enhance Workflow Management - Added and updated changesets for multiple PRs to improve tracking and documentation of changes. - Removed unnecessary test and dummy files (`test.txt`, various `.yml` fragments) to clean up the repository. - Refactored workflow scripts to streamline changelog generation and fragment handling, moving temporary files to a designated folder. - Updated GitHub Actions workflows by changing event triggers from `pull_request` to `pull_request_target` and vice versa to optimize workflow execution. - Enhanced security and automation by updating token names and adding write permissions to the changeset workflow. - Deleted obsolete workflow file for creating changeset files, now handled by an automated process. - Major clean-up of dummy fragment files and unnecessary changelog entries to maintain clarity and relevancy in documentation. - Implemented minor updates and improvements in codebase, specifically in generating release notes and handling fragments. --------- Signed-off-by: Johnathon Bowers <johnathonbowers@gmail.com> Signed-off-by: CMDWillYang <williamyang721@gmail.com> Signed-off-by: Qiwen Li <qiwen_li@brown.edu> Signed-off-by: qiwen li <qiwen_li@brown.edu> Signed-off-by: Samuel Valdes Gutierrez <valdesgutierrez@gmail.com> Signed-off-by: Ashwin P Chandran <ashwinpc1993@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Johnathon Bowers <johnathonbowers@gmail.com> Co-authored-by: CMDWillYang <williamyang721@gmail.com> Co-authored-by: Qiwen Li <qiwen_li@brown.edu> Co-authored-by: Ashwin P Chandran <ashwinpc1993@gmail.com> Co-authored-by: Anan Zhuang <ananzh@amazon.com> Co-authored-by: Josh Romero <rmerqg@amazon.com> Co-authored-by: autochangeset[bot] <154024398+autochangeset[bot]@users.noreply.github.com> Co-authored-by: opensearch-bot[bot] <154024398+opensearch-bot[bot]@users.noreply.github.com> Co-authored-by: opensearch-bot-dev[bot] <154634848+opensearch-bot-dev[bot]@users.noreply.github.com> Co-authored-by: Ashwin P Chandran <ashwinpc@amazon.com> Co-authored-by: Miki <amoo_miki@yahoo.com> Co-authored-by: Kawika Avilla <kavilla414@gmail.com>
1 parent 3fa2501 commit 603502b

7 files changed

+243
-1
lines changed

.github/pull_request_template.md

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@
77
<!-- List any issues this PR will resolve. -->
88
<!-- Example: closes #1234 -->
99

10+
## Changelog
11+
<!--
12+
Add a short but concise sentence about the impact of this pull request. Prefix an entry with the type of change they correspond to: breaking, chore, deprecate, doc, feat, fix, infra, refactor, test.
13+
- fix: Update the graph
14+
- feat: Add a new feature
15+
16+
If this change does not need to added to the changelog, just add a single `skip` line e.g.
17+
- skip
18+
19+
Descriptions following the prefixes must be 100 characters long or less
20+
-->
21+
1022
### Check List
1123

1224
- [ ] All tests pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: OpenSearch Changelog Workflow
2+
3+
on:
4+
pull_request_target:
5+
types: [opened, reopened, edited]
6+
7+
permissions:
8+
contents: read
9+
issues: write
10+
pull-requests: write
11+
12+
jobs:
13+
update-changelog:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Check out repository
17+
uses: actions/checkout@v4
18+
- name: Parse changelog entries and submit request for changset creation
19+
uses: BigSamu/OpenSearch_Changelog_Workflow@1.0.0-alpha1
20+
with:
21+
token: ${{secrets.GITHUB_TOKEN}}
22+
CHANGELOG_PR_BRIDGE_URL_DOMAIN: ${{secrets.CHANGELOG_PR_BRIDGE_URL_DOMAIN}}
23+
CHANGELOG_PR_BRIDGE_API_KEY: ${{secrets.CHANGELOG_PR_BRIDGE_API_KEY}}

changelogs/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog and Release Notes
2+
3+
For information regarding the changelog and release notes process, please consult the README in the GitHub Actions repository that this process utilizes. To view this README, follow the link below:
4+
5+
[GitHub Actions Workflow README](https://github.com/BigSamu/OpenSearch_Change_Set_Create_Action/blob/main/README.md)

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@
7575
"docs:acceptApiChanges": "scripts/use_node --max-old-space-size=6144 scripts/check_published_api_changes.js --accept",
7676
"osd:bootstrap": "scripts/use_node scripts/build_ts_refs && scripts/use_node scripts/register_git_hook",
7777
"spec_to_console": "scripts/use_node scripts/spec_to_console",
78-
"pkg-version": "scripts/use_node -e \"console.log(require('./package.json').version)\""
78+
"pkg-version": "scripts/use_node -e \"console.log(require('./package.json').version)\"",
79+
"release_note:generate": "scripts/use_node scripts/generate_release_note"
7980
},
8081
"repository": {
8182
"type": "git",

scripts/generate_release_note.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
require('../src/setup_node_env');
7+
require('../src/dev/generate_release_note');
8+
require('../src/dev/generate_release_note_helper');

src/dev/generate_release_note.ts

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { join, resolve } from 'path';
7+
import { readFileSync, writeFileSync, Dirent, rm, rename, promises as fsPromises } from 'fs';
8+
import { load as loadYaml } from 'js-yaml';
9+
import { readdir } from 'fs/promises';
10+
import { version as pkgVersion } from '../../package.json';
11+
import {
12+
validateFragment,
13+
getCurrentDateFormatted,
14+
Changelog,
15+
SECTION_MAPPING,
16+
fragmentDirPath,
17+
SectionKey,
18+
releaseNotesDirPath,
19+
filePath,
20+
} from './generate_release_note_helper';
21+
22+
// Function to add content after the 'Unreleased' section in the changelog
23+
function addContentAfterUnreleased(path: string, newContent: string): void {
24+
let fileContent = readFileSync(path, 'utf8');
25+
const targetString = '## [Unreleased]';
26+
const targetIndex = fileContent.indexOf(targetString);
27+
28+
if (targetIndex !== -1) {
29+
const endOfLineIndex = fileContent.indexOf('\n', targetIndex);
30+
if (endOfLineIndex !== -1) {
31+
fileContent =
32+
fileContent.slice(0, endOfLineIndex + 1) +
33+
'\n' +
34+
newContent +
35+
'\n' +
36+
fileContent.slice(endOfLineIndex + 1);
37+
} else {
38+
throw new Error('End of line for "Unreleased" section not found.');
39+
}
40+
} else {
41+
throw new Error("'## [Unreleased]' not found in the file.");
42+
}
43+
44+
writeFileSync(path, fileContent);
45+
}
46+
47+
async function deleteFragments(fragmentTempDirPath: string) {
48+
rm(fragmentTempDirPath, { recursive: true }, (err: any) => {
49+
if (err) {
50+
throw err;
51+
}
52+
});
53+
}
54+
55+
// Read fragment files and populate sections
56+
async function readFragments() {
57+
// Initialize sections
58+
const sections: Changelog = (Object.fromEntries(
59+
Object.keys(SECTION_MAPPING).map((key) => [key, []])
60+
) as unknown) as Changelog;
61+
62+
const fragmentPaths = await readdir(fragmentDirPath, { withFileTypes: true });
63+
for (const fragmentFilename of fragmentPaths) {
64+
// skip non yml or yaml files
65+
if (!/\.ya?ml$/i.test(fragmentFilename.name)) {
66+
// eslint-disable-next-line no-console
67+
console.warn(`Skipping non yml or yaml file ${fragmentFilename.name}`);
68+
continue;
69+
}
70+
71+
const fragmentPath = join(fragmentDirPath, fragmentFilename.name);
72+
const fragmentContents = readFileSync(fragmentPath, { encoding: 'utf-8' });
73+
74+
validateFragment(fragmentContents);
75+
76+
const fragmentYaml = loadYaml(fragmentContents) as Changelog;
77+
78+
for (const [sectionKey, entries] of Object.entries(fragmentYaml)) {
79+
sections[sectionKey as SectionKey].push(...entries);
80+
}
81+
}
82+
return { sections, fragmentPaths };
83+
}
84+
85+
async function moveFragments(fragmentPaths: Dirent[], fragmentTempDirPath: string): Promise<void> {
86+
// Move fragment files to temp fragments folder
87+
for (const fragmentFilename of fragmentPaths) {
88+
const fragmentPath = resolve(fragmentDirPath, fragmentFilename.name);
89+
const fragmentTempPath = resolve(fragmentTempDirPath, fragmentFilename.name);
90+
rename(fragmentPath, fragmentTempPath, () => {});
91+
}
92+
}
93+
94+
function generateChangelog(sections: Changelog) {
95+
// Generate changelog sections
96+
const changelogSections = Object.entries(sections).map(([sectionKey, entries]) => {
97+
const sectionName = SECTION_MAPPING[sectionKey as SectionKey];
98+
return entries.length === 0
99+
? `### ${sectionName}`
100+
: `### ${sectionName}\n\n${entries.map((entry) => ` - ${entry}`).join('\n')}`;
101+
});
102+
103+
// Generate full changelog
104+
const currentDate = getCurrentDateFormatted();
105+
const changelog = `## [${pkgVersion}-${currentDate}](https://github.com/opensearch-project/OpenSearch-Dashboards/releases/tag/${pkgVersion})\n\n${changelogSections.join(
106+
'\n\n'
107+
)}`;
108+
// Update changelog file
109+
addContentAfterUnreleased(filePath, changelog);
110+
return changelogSections;
111+
}
112+
113+
function generateReleaseNote(changelogSections: string[]) {
114+
// Generate release note
115+
const releaseNoteFilename = `opensearch-dashboards.release-notes-${pkgVersion}.md`;
116+
const releaseNoteHeader = `# VERSION ${pkgVersion} Release Note`;
117+
const releaseNote = `${releaseNoteHeader}\n\n${changelogSections.join('\n\n')}`;
118+
writeFileSync(resolve(releaseNotesDirPath, releaseNoteFilename), releaseNote);
119+
}
120+
121+
(async () => {
122+
const { sections, fragmentPaths } = await readFragments();
123+
// create folder for temp fragments
124+
const fragmentTempDirPath = await fsPromises.mkdtemp(join(fragmentDirPath, 'tmp_fragments-'));
125+
// move fragments to temp fragments folder
126+
await moveFragments(fragmentPaths, fragmentTempDirPath);
127+
128+
const changelogSections = generateChangelog(sections);
129+
130+
generateReleaseNote(changelogSections);
131+
132+
// remove temp fragments folder
133+
await deleteFragments(fragmentTempDirPath);
134+
})();
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { resolve } from 'path';
7+
8+
export const filePath = resolve(__dirname, '..', '..', 'CHANGELOG.md');
9+
export const fragmentDirPath = resolve(__dirname, '..', '..', 'changelogs', 'fragments');
10+
export const releaseNotesDirPath = resolve(__dirname, '..', '..', 'release-notes');
11+
12+
export function getCurrentDateFormatted(): string {
13+
return new Date().toISOString().slice(0, 10);
14+
}
15+
16+
export const SECTION_MAPPING = {
17+
breaking: '💥 Breaking Changes',
18+
deprecate: 'Deprecations',
19+
security: '🛡 Security',
20+
feat: '📈 Features/Enhancements',
21+
fix: '🐛 Bug Fixes',
22+
infra: '🚞 Infrastructure',
23+
doc: '📝 Documentation',
24+
chore: '🛠 Maintenance',
25+
refactor: '🪛 Refactoring',
26+
test: '🔩 Tests',
27+
};
28+
29+
export type SectionKey = keyof typeof SECTION_MAPPING;
30+
export type Changelog = Record<SectionKey, string[]>;
31+
32+
const MAX_ENTRY_LENGTH = 100;
33+
// Each entry must start with '-' and a space, followed by a non-empty string, and be no longer that MAX_ENTRY_LENGTH characters
34+
const entryRegex = new RegExp(`^-.{1,${MAX_ENTRY_LENGTH}}\\(\\[#.+]\\(.+\\)\\)$`);
35+
36+
// validate format of fragment files
37+
export function validateFragment(content: string) {
38+
const sections = content.split(/(?:\r?\n){2,}/);
39+
40+
// validate each section
41+
for (const section of sections) {
42+
const lines = section.split('\n');
43+
const sectionName = lines[0];
44+
const sectionKey = sectionName.slice(0, -1);
45+
46+
if (!SECTION_MAPPING[sectionKey as SectionKey] || !sectionName.endsWith(':')) {
47+
throw new Error(`Unknown section ${sectionKey}.`);
48+
}
49+
for (const entry of lines.slice(1)) {
50+
if (entry === '') {
51+
continue;
52+
}
53+
// if (!entryRegex.test(entry)) {
54+
if (!entryRegex.test(entry.trim())) {
55+
throw new Error(`Invalid entry ${entry} in section ${sectionKey}.`);
56+
}
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)