Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -225,3 +225,4 @@ Tools/**/*.js.map

!jest.config.js
output/release-notes.md
output/release-notes.html
2 changes: 1 addition & 1 deletion CommitRangeReleaseNotesTask/task/dist/.taskkey
Original file line number Diff line number Diff line change
@@ -1 +1 @@
30435434-173e-4a63-8c43-9a886bd70284
8540612f-cbf9-4737-b3b4-6f49e7425db8
3 changes: 2 additions & 1 deletion CommitRangeReleaseNotesTask/task/dist/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const fs = require("fs");
const CommitUtils_1 = require("./utils/CommitUtils");
const PRUtils_1 = require("./utils/PRUtils");
const TemplateUtils_1 = require("./utils/TemplateUtils");
const JsonOutput_1 = require("./utils/JsonOutput");
(0, TemplateUtils_1.registerHelpers)();
function GenerateReleaseNotes(startCommit, endCommit, outputFileMarkdown, outputFileHtml, repoRoot, systemAccessToken, project, apiUrl, repositoryId, templateFileMarkdown, templateFileHtml) {
return __awaiter(this, void 0, void 0, function* () {
Expand Down Expand Up @@ -157,7 +158,7 @@ function GenerateReleaseNotes(startCommit, endCommit, outputFileMarkdown, output
repositoryId,
project
};
//printJson(releaseData);
(0, JsonOutput_1.printJson)(releaseData);
(0, TemplateUtils_1.GenerateMarkdownReleaseNotes)(releaseData, outputFileMarkdown, templateFileMarkdown);
(0, TemplateUtils_1.GenerateHtmlReleaseNotes)(releaseData, outputFileHtml, templateFileHtml);
});
Expand Down
28 changes: 0 additions & 28 deletions CommitRangeReleaseNotesTask/task/dist/utils/TemplateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ const Handlebars = __importStar(require("handlebars"));
const fs = require("fs");
const path = require("path");
const tl = require("azure-pipelines-task-lib/task");
const JsonOutput_1 = require("./JsonOutput");
function registerHelpers() {
// GroupBy helper
Handlebars.registerHelper('groupBy', function (items, field, options) {
Expand Down Expand Up @@ -104,72 +103,45 @@ function GenerateMarkdownReleaseNotes(data, outputFile, templateFile) {
}
exports.GenerateMarkdownReleaseNotes = GenerateMarkdownReleaseNotes;
function GenerateHtmlReleaseNotes(data, outputFile, templateFile) {
var _a, _b;
console.log('Starting HTML release notes generation...');
// Convert PR descriptions from markdown to HTML
const { marked } = require('marked');
console.log(`Found ${((_a = data.pullRequests) === null || _a === void 0 ? void 0 : _a.length) || 0} pull requests`);
console.log(`Found ${((_b = data.commits) === null || _b === void 0 ? void 0 : _b.length) || 0} commits`);
// Convert descriptions in the pullRequests array
if (Array.isArray(data.pullRequests) && data.pullRequests.length > 0) {
console.log(`Processing ${data.pullRequests.length} pull requests`);
data.pullRequests = data.pullRequests.map(pr => {
console.log(`Checking PR ${pr.id}: has description = ${!!pr.description}, type = ${typeof pr.description}`);
if (pr && pr.description != null && typeof pr.description === 'string' && pr.description.trim() !== '') {
console.log(`Converting PR ${pr.id} description from markdown to HTML`);
console.log(`Original (first 200 chars): ${pr.description.substring(0, 200)}`);
try {
const converted = marked(pr.description);
console.log(`Converted (first 200 chars): ${converted.substring(0, 200)}`);
return Object.assign(Object.assign({}, pr), { description: converted });
}
catch (error) {
console.warn(`Failed to convert markdown for PR: ${pr.id || 'unknown'}`, error);
return pr;
}
}
else {
console.log(`Skipping PR ${pr.id} - no valid description`);
}
return pr;
});
}
else {
console.log('No pull requests to process');
}
// Convert descriptions in commits.pullRequest objects
if (Array.isArray(data.commits) && data.commits.length > 0) {
console.log(`Processing ${data.commits.length} commits`);
data.commits = data.commits.map(commit => {
if (commit && commit.pullRequest) {
console.log(`Checking commit PR ${commit.pullRequest.id}: has description = ${!!commit.pullRequest.description}, type = ${typeof commit.pullRequest.description}`);
if (commit.pullRequest.description != null &&
typeof commit.pullRequest.description === 'string' &&
commit.pullRequest.description.trim() !== '') {
console.log(`Converting commit PR ${commit.pullRequest.id} description from markdown to HTML`);
console.log(`Original (first 200 chars): ${commit.pullRequest.description.substring(0, 200)}`);
try {
const converted = marked(commit.pullRequest.description);
console.log(`Converted (first 200 chars): ${converted.substring(0, 200)}`);
return Object.assign(Object.assign({}, commit), { pullRequest: Object.assign(Object.assign({}, commit.pullRequest), { description: converted }) });
}
catch (error) {
console.warn(`Failed to convert markdown for commit PR: ${commit.pullRequest.id || 'unknown'}`, error);
return commit;
}
}
else {
console.log(`Skipping commit PR ${commit.pullRequest.id} - no valid description`);
}
}
return commit;
});
}
else {
console.log('No commits to process');
}
console.log('Markdown conversion complete, proceeding with template generation...');
(0, JsonOutput_1.printJson)(data);
// Get template
let template;
try {
Expand Down
7 changes: 0 additions & 7 deletions CommitRangeReleaseNotesTask/task/dist/utils/WorkItemUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ exports.getWorkItem = void 0;
function getWorkItem(workItemId, apiUrl, project, accessToken) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function* () {
const fields = [
"System.Title",
"System.WorkItemType",
"System.AssignedTo",
"System.Description"
];
// fields=${fields.join(',')}&
const url = `${apiUrl}/${project}/_apis/wit/workitems/${workItemId}?api-version=7.1&$expand=ALL`;
console.log(`Fetching work item ${workItemId} from ${url}`);
try {
Expand Down
4 changes: 2 additions & 2 deletions CommitRangeReleaseNotesTask/task/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@
"name": "outputFileMarkdown",
"type": "filePath",
"label": "Output File - Markdown",
"defaultValue": "$(Build.ArtifactStagingDirectory)/ReleaseNotes.md",
"defaultValue": "$(Build.ArtifactStagingDirectory)/release-notes.md",
"required": true,
"helpMarkDown": "Path where the generated release notes will be saved"
},
{
"name": "outputFileHtml",
"type": "filePath",
"label": "Output File - HTML",
"defaultValue": "$(Build.ArtifactStagingDirectory)/ReleaseNotes.html",
"defaultValue": "$(Build.ArtifactStagingDirectory)/release-notes.html",
"required": true,
"helpMarkDown": "Path where the generated release notes will be saved"
},
Expand Down
8 changes: 0 additions & 8 deletions CommitRangeReleaseNotesTask/task/utils/WorkItemUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,6 @@ export async function getWorkItem(
project: string,
accessToken: string
): Promise<WorkItem | null> {
const fields = [
"System.Title",
"System.WorkItemType",
"System.AssignedTo",
"System.Description"
];

// fields=${fields.join(',')}&
const url = `${apiUrl}/${project}/_apis/wit/workitems/${workItemId}?api-version=7.1&$expand=ALL`;
console.log(`Fetching work item ${workItemId} from ${url}`);

Expand Down
38 changes: 22 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ The extension analyses Git commits in a specified range, looking for merge commi
inputs:
startCommit: 'v1.0.0'
endCommit: 'HEAD'
outputFile: '$(Build.ArtifactStagingDirectory)/ReleaseNotes.md'
outputFileMarkdown: '$(Build.ArtifactStagingDirectory)/release-notes.md'
outputFileHtml: '$(Build.ArtifactStagingDirectory)/release-notes.html'
```

### With Custom Template
Expand All @@ -33,25 +34,27 @@ The extension analyses Git commits in a specified range, looking for merge commi
inputs:
startCommit: 'v1.0.0'
endCommit: 'v1.1.0'
outputFile: 'release-notes.md'
templateFile: 'templates/custom-release-notes.hbs'
outputFileMarkdown: '$(Build.ArtifactStagingDirectory)/release-notes.md'
outputFileHtml: '$(Build.ArtifactStagingDirectory)/release-notes.html'
templateFileMarkdown: 'templates/custom-release-notes.hbs'
templateFileHtml: 'templates/custom-release-notes.hbs'
```

## Parameters
| Parameter | Description | Required | Default |
|-----------|-------------|----------|---------|
| `startCommit` | Commit reference for the start of the range (exclusive). Can be a commit hash, git tag, or a ref like `HEAD` or `HEAD~xx` | ✅ | - |
| `endCommit` | Commit reference for the end of the range (inclusive). Can be a commit hash, git tag, or a ref like `HEAD` or `HEAD~xx` | ✅ | `HEAD` |
| `outputFile` | Path where generated release notes will be saved | ✅ | `$(Build.ArtifactStagingDirectory)/ReleaseNotes.md` |
| `templateFile` | Path to custom Handlebars template file | ❌ | Built-in template |
| `outputFileMarkdown` and `outputFileHtml` | Path where generated release notes will be saved | ✅ | `$(Build.ArtifactStagingDirectory)/release-notes.html` and `$(Build.ArtifactStagingDirectory)/release-notes.html` |
| `templateFileMarkdown` and `templateFileHtml` | Path to custom Handlebars template file | ❌ | Built-in template |

### Supported commit reference formats for `startCommit` and `endCommit`
- Commit hash (e.g., `a1b2c3d`)
- Git tag (e.g., `v1.0.0`)
- `HEAD` or `HEAD~xx` (where `xx` is the number of commits before HEAD)

## Sample Output
The [default template](https://github.com/IeuanWalker/AzureDevops-GenerateReleaseNotes/blob/master/CommitRangeReleaseNotesTask/task/defaultTemplate.hbs) outputs the following format -
## Output
The [default markdown template](https://github.com/IeuanWalker/AzureDevops-GenerateReleaseNotes/blob/master/CommitRangeReleaseNotesTask/task/defaultTemplateMarkdown.hbs) outputs the following format -
```markdown
## 📊 Summary
- **3** Pull Requests
Expand Down Expand Up @@ -85,6 +88,8 @@ The [default template](https://github.com/IeuanWalker/AzureDevops-GenerateReleas
| [44](https://dev.azure.com/org/project/_git/pullrequest/44) | Bug fixes and improvements | Bob Wilson |
```

It also generates an interactive html version using this [template.](https://github.com/IeuanWalker/AzureDevops-GenerateReleaseNotes/blob/master/CommitRangeReleaseNotesTask/task/defaultTemplateHtml.hbs)

## Template Customisation
The task uses Handlebars templates to format output. You can provide a custom template file or use the built-in default template.

Expand Down Expand Up @@ -192,21 +197,22 @@ interface WorkItemList {
{{/if}}
```

## Console Usage
## Local testing
The task can also be run from the command line for testing.

- Clone the repo
- Run `npm run build`, in the `CommitRangeReleaseNotesTask` folder
- Run the following command from the folder `CommitRangeReleaseNotesTask\task\dist`
```cmd
node ./mainConsole.js \
--startCommit "v1.0.0" \
--endCommit "HEAD" \
--outputFile "C:\release-notes.md" \
--repoRoot "\path\to\repo" \
--systemAccessToken "your-token" \
--project "your-project" \
--repositoryId "your-repo" \
node ./mainConsole.js
--startCommit "v1.0.0"
--endCommit "HEAD"
--outputFileMarkdown "C:\release-notes.md"
--outputFileHtml "C:\release-notes.html"
--repoRoot "\path\to\repo"
--systemAccessToken "your-token"
--project "your-project"
--repositoryId "your-repo"
--apiUrl "https://dev.azure.com/your-org"
```
> To test locally, the `systemAccessToken` will need to be a [Personal Access Token (PAT)](https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=Windows).
Loading