Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
29 changes: 29 additions & 0 deletions .github/workflows/update-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Update CHANGELOG
on:
workflow_dispatch

permissions:
contents: write
pull-requests: write

jobs:
update-changelog:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
- name: Install NodeJS
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install dependencies
run: npm ci
- name: Update version.json
run: npx gulp updateChangelog
- name: Create version update PR
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update ${{ github.ref_name }} CHANGELOG
title: '[automated] Update ${{ github.ref_name }} CHANGELOG'
branch: merge/update-${{ github.ref_name }}-changelog
14 changes: 14 additions & 0 deletions tasks/gitTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { spawnSync } from 'child_process';
import { Octokit } from '@octokit/rest';
import { EOL } from 'os';

/**
* Execute a git command with optional logging
Expand Down Expand Up @@ -146,3 +147,16 @@ export async function createPullRequest(
return null;
}
}

/**
* Find all tags that match the given version pattern
* @param version The `Major.Minor` version pattern to match
* @returns A sorted list of matching tags from oldest to newest
*/
export async function findTagsByVersion(version: string): Promise<string[]> {
const tagList = await git(['tag', '--list', `v${version}*`, '--sort=creatordate'], false);
return tagList
.split(EOL)
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
}
122 changes: 122 additions & 0 deletions tasks/snapTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ import * as gulp from 'gulp';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { exec } from 'child_process';
import { promisify } from 'util';
import { findTagsByVersion } from './gitTasks';

const execAsync = promisify(exec);

function logWarning(message: string, error?: unknown): void {
console.log(`##vso[task.logissue type=warning]${message}`);
if (error instanceof Error && error.stack) {
console.log(`##[debug]${error.stack}`);
}
}

gulp.task('incrementVersion', async (): Promise<void> => {
// Get the current version from version.json
Expand Down Expand Up @@ -64,3 +76,113 @@ gulp.task('incrementVersion', async (): Promise<void> => {
changelogLines.splice(lineToInsertAt, 0, ...linesToInsert);
fs.writeFileSync(changelogPath, changelogLines.join(os.EOL));
});

gulp.task('updateChangelog', async (): Promise<void> => {
// Add a new changelog section for the new version.
console.log('Determining version from CHANGELOG');

const changelogPath = path.join(path.resolve(__dirname, '..'), 'CHANGELOG.md');
const changelogContent = fs.readFileSync(changelogPath, 'utf8');
const changelogLines = changelogContent.split(os.EOL);

// Find all the headers in the changelog (and their line numbers)
const [currentHeaderLine, currentVersion] = findNextVersionHeaderLine(changelogLines);
if (currentHeaderLine === -1) {
throw new Error('Could not find the current header in the CHANGELOG');
}

console.log(`Adding PRs for ${currentVersion} to CHANGELOG`);

const [previousHeaderLine, previousVersion] = findNextVersionHeaderLine(changelogLines, currentHeaderLine + 1);
if (previousHeaderLine === -1) {
throw new Error('Could not find the previous header in the CHANGELOG');
}

const presentPrIds = getPrIdsBetweenHeaders(changelogLines, currentHeaderLine, previousHeaderLine);
console.log(`PRs [#${presentPrIds.join(', #')}] already in the CHANGELOG`);

const versionTags = await findTagsByVersion(previousVersion!);
if (versionTags.length === 0) {
throw new Error(`Could not find any tags for version ${previousVersion}`);
}

// The last tag is the most recent one created.
const versionTag = versionTags.pop();
console.log(`Using tag ${versionTag} for previous version ${previousVersion}`);

console.log(`Generating PR list from ${versionTag} to HEAD`);
const currentPrs = await generatePRList(versionTag!, 'HEAD');

const newPrs = [];
for (const pr of currentPrs) {
const match = prRegex.exec(pr);
if (!match) {
continue;
}

const prId = match[1];
if (presentPrIds.includes(prId)) {
console.log(`PR #${prId} is already present in the CHANGELOG`);
continue;
}

console.log(`Adding new PR to CHANGELOG: ${pr}`);
newPrs.push(pr);
}

if (newPrs.length === 0) {
console.log('No new PRs to add to the CHANGELOG');
return;
}

console.log(`Writing ${newPrs.length} new PRs to the CHANGELOG`);

changelogLines.splice(currentHeaderLine + 1, 0, ...newPrs);
fs.writeFileSync(changelogPath, changelogLines.join(os.EOL));
});

const prRegex = /^\*.+\(PR: \[#(\d+)\]\(/g;

function findNextVersionHeaderLine(changelogLines: string[], startLine: number = 0): [number, string] {
const headerRegex = /^#\s(\d+\.\d+)\.(x|\d+)$/gm;
for (let i = startLine; i < changelogLines.length; i++) {
const line = changelogLines.at(i);
const match = headerRegex.exec(line!);
if (match) {
return [i, match[1]];
}
}
return [-1, ''];
}

function getPrIdsBetweenHeaders(changelogLines: string[], startLine: number, endLine: number): string[] {
const prs: string[] = [];
for (let i = startLine; i < endLine; i++) {
const line = changelogLines.at(i);
const match = prRegex.exec(line!);
if (match && match[1]) {
prs.push(match[1]);
}
}
return prs;
}

async function generatePRList(startSHA: string, endSHA: string): Promise<string[]> {
try {
console.log(`Executing: roslyn-tools pr-finder -s "${startSHA}" -e "${endSHA}" --format o#`);
let { stdout } = await execAsync(
`roslyn-tools pr-finder -s "${startSHA}" -e "${endSHA}" --format o#`,
{ maxBuffer: 10 * 1024 * 1024 } // 10MB buffer
);

stdout = stdout.trim();
if (stdout.length === 0) {
return [];
}

return stdout.split(os.EOL).filter((pr) => pr.length > 0);
} catch (error) {
logWarning(`PR finder failed: ${error instanceof Error ? error.message : error}`, error);
return [];
}
}
Loading