Skip to content
This repository has been archived by the owner on May 5, 2024. It is now read-only.

feat: add support for GitHub automatic release notes generation #442

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
10 changes: 7 additions & 3 deletions packages/automatic-releases/__tests__/automaticReleases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('main handler processing automatic releases', () => {
const testInputTitle = 'Development Build';
const testInputBody = `## Commits\n- f6f40d9: Fix all the bugs (Monalisa Octocat)`;
const testInputFiles = 'file1.txt\nfile2.txt\n*.jar\n\n';
const testInputAutoGenerateReleaseNotes = false;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -27,6 +28,7 @@ describe('main handler processing automatic releases', () => {
process.env['INPUT_PRERELEASE'] = testInputPrerelease.toString();
process.env['INPUT_TITLE'] = testInputTitle;
process.env['INPUT_FILES'] = testInputFiles;
process.env['INPUT_AUTO_GENERATE_RELEASE_NOTES'] = testInputAutoGenerateReleaseNotes.toString();

process.env['GITHUB_EVENT_NAME'] = 'push';
process.env['GITHUB_SHA'] = testGhSHA;
Expand Down Expand Up @@ -93,14 +95,15 @@ describe('main handler processing automatic releases', () => {
.delete(/.*/)
.reply(200);

const createRelease = nock('https://api.github.com')
.matchHeader('authorization', `token ${testGhToken}`)
.post('/repos/marvinpinto/private-actions-tester/releases', {
const createRelease = nock("https://api.github.com")
.matchHeader("authorization", `token ${testGhToken}`)
.post("/repos/marvinpinto/private-actions-tester/releases", {
tag_name: testInputAutomaticReleaseTag,
name: testInputTitle,
draft: testInputDraft,
prerelease: testInputPrerelease,
body: testInputBody,
generate_release_notes: testInputAutoGenerateReleaseNotes,
})
.reply(200, {
upload_url: releaseUploadUrl,
Expand Down Expand Up @@ -195,6 +198,7 @@ describe('main handler processing automatic releases', () => {
draft: testInputDraft,
prerelease: testInputPrerelease,
body: testInputBody,
generate_release_notes: testInputAutoGenerateReleaseNotes,
})
.reply(200, {
upload_url: releaseUploadUrl,
Expand Down
13 changes: 8 additions & 5 deletions packages/automatic-releases/__tests__/taggedReleases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe('main handler processing tagged releases', () => {
const testInputPrerelease = false;
const testInputBody = `## Commits\n- f6f40d9: Fix all the bugs (Monalisa Octocat)`;
const testInputFiles = 'file1.txt\nfile2.txt\n*.jar\n\n';
const testInputAutoGenerateReleaseNotes = false;

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -23,6 +24,7 @@ describe('main handler processing tagged releases', () => {
process.env['INPUT_DRAFT'] = testInputDraft.toString();
process.env['INPUT_PRERELEASE'] = testInputPrerelease.toString();
process.env['INPUT_FILES'] = testInputFiles;
process.env['INPUT_AUTO_GENERATE_RELEASE_NOTES'] = testInputAutoGenerateReleaseNotes.toString();

process.env['GITHUB_EVENT_NAME'] = 'push';
process.env['GITHUB_SHA'] = testGhSHA;
Expand Down Expand Up @@ -103,14 +105,15 @@ describe('main handler processing tagged releases', () => {
.get(`/repos/marvinpinto/private-actions-tester/commits/${testGhSHA}/pulls`)
.reply(200, []);

const createRelease = nock('https://api.github.com')
.matchHeader('authorization', `token ${testGhToken}`)
.post('/repos/marvinpinto/private-actions-tester/releases', {
tag_name: 'v0.0.1',
name: 'v0.0.1',
const createRelease = nock("https://api.github.com")
.matchHeader("authorization", `token ${testGhToken}`)
.post("/repos/marvinpinto/private-actions-tester/releases", {
tag_name: "v0.0.1",
name: "v0.0.1",
draft: testInputDraft,
prerelease: testInputPrerelease,
body: testInputBody,
generate_release_notes: testInputAutoGenerateReleaseNotes,
})
.reply(200, {
upload_url: releaseUploadUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const testInputDraft = false;
const testInputPrerelease = true;
const testInputTitle = 'Development Build';
const testInputFiles = 'file1.txt\nfile2.txt\n*.jar\n\n';
const testInputAutoGenerateReleaseNotes = false;

server.get(`/repos/marvinpinto/private-actions-tester/compare/HEAD...${testGhSHA}`, (req, res) => {
const compareCommitsPayload = JSON.parse(
Expand Down Expand Up @@ -49,6 +50,7 @@ export const setupEnv = {
INPUT_PRERELEASE: testInputPrerelease.toString(),
INPUT_TITLE: testInputTitle,
INPUT_FILES: testInputFiles,
INPUT_AUTO_GENERATE_RELEASE_NOTES: testInputAutoGenerateReleaseNotes.toString(),

GITHUB_EVENT_NAME: 'push',
GITHUB_SHA: testGhSHA,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const testGhSHA = 'f6f40d9fbd1130f7f2357bb54225567dbd7a3793';
const testInputDraft = false;
const testInputPrerelease = true;
const testInputFiles = 'file1.txt\nfile2.txt\n*.jar\n\n';
const testInputAutoGenerateReleaseNotes = false;

server.get(`/repos/marvinpinto/private-actions-tester/tags`, (req, res) => {
res.json([
Expand Down Expand Up @@ -65,6 +66,7 @@ export const setupEnv = {
INPUT_DRAFT: testInputDraft.toString(),
INPUT_PRERELEASE: testInputPrerelease.toString(),
INPUT_FILES: testInputFiles,
INPUT_AUTO_GENERATE_RELEASE_NOTES: testInputAutoGenerateReleaseNotes.toString(),

GITHUB_EVENT_NAME: 'push',
GITHUB_SHA: testGhSHA,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const testInputDraft = false;
const testInputPrerelease = true;
const testInputTitle = 'Development Build';
const testInputFiles = 'file1.txt\nfile2.txt\n*.jar\n\n';
const testInputAutoGenerateReleaseNotes = false;

const previousReleaseSHA = '4398ef4ea6f5a61880ca94ecfb8e60d1a38497dd';
const foundReleaseId = 1235523222;
Expand Down Expand Up @@ -67,6 +68,7 @@ export const setupEnv = {
INPUT_PRERELEASE: testInputPrerelease.toString(),
INPUT_TITLE: testInputTitle,
INPUT_FILES: testInputFiles,
INPUT_AUTO_GENERATE_RELEASE_NOTES: testInputAutoGenerateReleaseNotes.toString(),

GITHUB_EVENT_NAME: 'push',
GITHUB_SHA: testGhSHA,
Expand Down
4 changes: 4 additions & 0 deletions packages/automatic-releases/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ inputs:
files:
description: "Assets to upload to the release"
required: false
auto_generate_release_notes:
description: "Let GitHub generate release notes automatically"
required: false
default: false
outputs:
automatic_releases_tag:
description: "The release tag this action just processed"
Expand Down
3 changes: 2 additions & 1 deletion packages/automatic-releases/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/github": "2.0.0",
"@octokit/rest": "16.36.0",
"@octokit/rest": "^18.12.0",
"@octokit/types": "^6.34.0",
"conventional-changelog-angular": "^5.0.12",
"conventional-commits-parser": "^3.2.0",
"globby": "^11.0.1",
Expand Down
35 changes: 23 additions & 12 deletions packages/automatic-releases/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import {Context} from '@actions/github/lib/context';
import * as Octokit from '@octokit/rest';
import {Endpoints} from '@octokit/types';
import {dumpGitHubEventPayload} from '../../keybase-notifications/src/utils';
import {sync as commitParser} from 'conventional-commits-parser';
import {getChangelogOptions} from './utils';
import {getChangelogOptions, ReposCompareCommitsResponseCommitsItem} from './utils';
import {isBreakingChange, generateChangelogFromParsedCommits, parseGitTag, ParsedCommits, octokitLogger} from './utils';
import semverValid from 'semver/functions/valid';
import semverRcompare from 'semver/functions/rcompare';
import semverLt from 'semver/functions/lt';
import {uploadReleaseArtifacts} from './uploadReleaseArtifacts';

type GitCreateRefParams = Endpoints['POST /repos/{owner}/{repo}/git/refs']['parameters'];
type GitGetRefParams = Endpoints['GET /repos/{owner}/{repo}/git/ref/{ref}']['parameters'];
type ReposListTagsParams = Endpoints['GET /repos/{owner}/{repo}/tags']['parameters'];
type ReposGetReleaseByTagParams = Endpoints['GET /repos/{owner}/{repo}/releases/tags/{tag}']['parameters'];
type ReposCreateReleaseParams = Endpoints['POST /repos/{owner}/{repo}/releases']['parameters'];

type Args = {
repoToken: string;
automaticReleaseTag: string;
draftRelease: boolean;
preRelease: boolean;
releaseTitle: string;
files: string[];
autoGenerateReleaseNotes: boolean;
};

const getAndValidateArgs = (): Args => {
const args = {
repoToken: core.getInput('repo_token', {required: true}),
automaticReleaseTag: core.getInput('automatic_release_tag', {required: false}),
automaticReleaseTag: core.getInput('automatic_release_tag', {
required: false,
}),
draftRelease: JSON.parse(core.getInput('draft', {required: true})),
preRelease: JSON.parse(core.getInput('prerelease', {required: true})),
releaseTitle: core.getInput('title', {required: false}),
files: [] as string[],
autoGenerateReleaseNotes: JSON.parse(core.getInput('auto_generate_release_notes', {required: true})),
};

const inputFilesStr = core.getInput('files', {required: false});
Expand All @@ -38,7 +48,7 @@ const getAndValidateArgs = (): Args => {
return args;
};

const createReleaseTag = async (client: github.GitHub, refInfo: Octokit.GitCreateRefParams) => {
const createReleaseTag = async (client: github.GitHub, refInfo: GitCreateRefParams) => {
core.startGroup('Generating release tag');
const friendlyTagName = refInfo.ref.substring(10); // 'refs/tags/latest' => 'latest'
core.info(`Attempting to create or update release tag "${friendlyTagName}"`);
Expand All @@ -61,7 +71,7 @@ const createReleaseTag = async (client: github.GitHub, refInfo: Octokit.GitCreat
core.endGroup();
};

const deletePreviousGitHubRelease = async (client: github.GitHub, releaseInfo: Octokit.ReposGetReleaseByTagParams) => {
const deletePreviousGitHubRelease = async (client: github.GitHub, releaseInfo: ReposGetReleaseByTagParams) => {
core.startGroup(`Deleting GitHub releases associated with the tag "${releaseInfo.tag}"`);
try {
core.info(`Searching for releases corresponding to the "${releaseInfo.tag}" tag`);
Expand All @@ -81,7 +91,7 @@ const deletePreviousGitHubRelease = async (client: github.GitHub, releaseInfo: O

const generateNewGitHubRelease = async (
client: github.GitHub,
releaseInfo: Octokit.ReposCreateReleaseParams,
releaseInfo: ReposCreateReleaseParams,
): Promise<string> => {
core.startGroup(`Generating new GitHub release for the "${releaseInfo.tag_name}" tag`);

Expand All @@ -94,7 +104,7 @@ const generateNewGitHubRelease = async (
const searchForPreviousReleaseTag = async (
client: github.GitHub,
currentReleaseTag: string,
tagInfo: Octokit.ReposListTagsParams,
tagInfo: ReposListTagsParams,
): Promise<string> => {
const validSemver = semverValid(currentReleaseTag);
if (!validSemver) {
Expand Down Expand Up @@ -131,9 +141,9 @@ const searchForPreviousReleaseTag = async (

const getCommitsSinceRelease = async (
client: github.GitHub,
tagInfo: Octokit.GitGetRefParams,
tagInfo: GitGetRefParams,
currentSha: string,
): Promise<Octokit.ReposCompareCommitsResponseCommitsItem[]> => {
): Promise<ReposCompareCommitsResponseCommitsItem[]> => {
core.startGroup('Retrieving commit history');
let resp;

Expand Down Expand Up @@ -180,7 +190,7 @@ export const getChangelog = async (
client: github.GitHub,
owner: string,
repo: string,
commits: Octokit.ReposCompareCommitsResponseCommitsItem[],
commits: ReposCompareCommitsResponseCommitsItem[],
): Promise<string> => {
const parsedCommits: ParsedCommits[] = [];
core.startGroup('Generating changelog');
Expand Down Expand Up @@ -306,10 +316,11 @@ export const main = async (): Promise<void> => {
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: releaseTag,
name: args.releaseTitle ? args.releaseTitle : releaseTag,
...(args.autoGenerateReleaseNotes ? {} : {name: args.releaseTitle ? args.releaseTitle : releaseTag}),
Copy link

@natarajanc-prodigygame natarajanc-prodigygame Jun 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lauravuo i tried using this via your forked action.yml https://github.com/lauravuo/action-automatic-releases/blob/test-changes/action.yml for some reason the custom title is not getting reflected when i generate a release (it uses the automatic_release_tag as the release title)

im using the action this way

      - uses: "lauravuo/action-automatic-releases@test-changes"
        with:
          repo_token: ${{ secrets.OPS_GHA_USER_TOKEN }}
          draft: false
          title: "Dippr release - ${{ steps.date.outputs.date }}"
          automatic_release_tag: ${{ steps.get-latest-tag.outputs.tag }}
          prerelease: false
          auto_generate_release_notes: true

draft: args.draftRelease,
prerelease: args.preRelease,
body: changelog,
...(args.autoGenerateReleaseNotes ? {} : {body: changelog}),
generate_release_notes: args.autoGenerateReleaseNotes,
});

await uploadReleaseArtifacts(client, releaseUploadUrl, args.files);
Expand Down
20 changes: 17 additions & 3 deletions packages/automatic-releases/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import * as core from '@actions/core';
import * as Octokit from '@octokit/rest';
import {Octokit} from '@octokit/rest';

import defaultChangelogOpts from 'conventional-changelog-angular/conventional-recommended-bump';

const octokit = new Octokit();

declare type Unwrap<T> = T extends Promise<infer U> ? U : T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare type AnyFunction = (...args: any[]) => any;
declare type GetResponseDataCommitsTypeFromEndpointMethod<T extends AnyFunction> = Unwrap<
ReturnType<T>
>['data']['commits'][0];

export type ReposCompareCommitsResponseCommitsItem = GetResponseDataCommitsTypeFromEndpointMethod<
typeof octokit.repos.compareCommits
>;

export const getShortSHA = (sha: string): string => {
const coreAbbrev = 7;
return sha.substring(0, coreAbbrev);
};

export type ParsedCommitsExtraCommit = Octokit.ReposCompareCommitsResponseCommitsItem & {
export type ParsedCommitsExtraCommit = ReposCompareCommitsResponseCommitsItem & {
author: {
email: string;
name: string;
Expand Down Expand Up @@ -79,7 +93,7 @@ const getFormattedChangelogEntry = (parsedCommit: ParsedCommits): string => {

const url = parsedCommit.extra.commit.html_url;
const sha = getShortSHA(parsedCommit.extra.commit.sha);
const author = parsedCommit.extra.commit.commit.author.name;
const author = parsedCommit.extra.commit.commit.author?.name;

let prString = '';
prString = parsedCommit.extra.pullRequests.reduce((acc, pr) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/keybase-notifications/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ export const dumpGitHubEventPayload = (): void => {
}
const contents = fs.readFileSync(ghpath, 'utf8');
const jsonContent = JSON.parse(contents);
core.info(`GitHub payload: ${JSON.stringify(jsonContent)}`);
core.debug(`GitHub payload: ${JSON.stringify(jsonContent)}`);
};
Loading