Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Setup automatic weekly changelog updates #180

Closed
wants to merge 6 commits into from
Closed
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
16 changes: 16 additions & 0 deletions .github/workflows/update-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Update changelog

on:
workflow_dispatch:
schedule:
# Run every Sunday at 0:00
- cron: '0 0 * * 0'

jobs:
update_changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./tools/local-actions/changelog
with:
angular-robot-key: ${{ secrets.ANGULAR_ROBOT_PRIVATE_KEY }}
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ github-actions/breaking-changes-label/main.js
github-actions/breaking-changes-label/post.js
github-actions/slash-commands/main.js
github-actions/slash-commands/post.js
tools/local-actions/changelog/main.js
tools/local-actions/changelog/post.js
Empty file added CHANGELOG.md
Empty file.
4 changes: 4 additions & 0 deletions github-actions/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ ts_library(
srcs = [
"utils.ts",
],
visibility = [
"//github-actions:__subpackages__",
"//tools/local-actions:__subpackages__",
],
deps = [
"@npm//@actions/core",
"@npm//@actions/github",
Expand Down
12 changes: 9 additions & 3 deletions github-actions/slash-commands/lib/commands/rebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import {Octokit} from '@octokit/rest';
import {context} from '@actions/github';
import {getAuthTokenFor, ANGULAR_ROBOT} from '../../../utils';
import {AuthenticatedGitClient} from '../../../../ng-dev/utils/git/authenticated-git-client';
import {setConfig} from '../../../../ng-dev/utils/config';

export async function rebase() {
const token = await getAuthTokenFor(ANGULAR_ROBOT);
const {owner, repo: name} = context.repo;
const mainBranchName = context.payload.repository!.default_branch;
setConfig({
github: {
name: context.repo.repo,
owner: context.repo.owner,
mainBranchName: context.payload.repository!.default_branch,
},
});

AuthenticatedGitClient.configureForGithubActions(token, {github: {name, owner, mainBranchName}});
AuthenticatedGitClient.configure(token);

if ((await rebasePr(context.issue.number, token)) !== 0) {
// For any failure to rebase, comment on the PR informing the user a rebase was unable to be
Expand Down
29 changes: 15 additions & 14 deletions github-actions/slash-commands/main.js

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion ng-dev/release/config/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ ts_library(
srcs = glob([
"**/*.ts",
]),
visibility = ["//ng-dev:__subpackages__"],
visibility = [
"//ng-dev:__subpackages__",
"//tools/local-actions/changelog/lib:__subpackages__",
],
deps = [
"//ng-dev/commit-message",
"//ng-dev/utils",
Expand Down
5 changes: 4 additions & 1 deletion ng-dev/release/notes/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ ts_library(
srcs = glob([
"**/*.ts",
]),
visibility = ["//ng-dev:__subpackages__"],
visibility = [
"//ng-dev:__subpackages__",
"//tools/local-actions/changelog/lib:__subpackages__",
],
deps = [
"//ng-dev/commit-message",
"//ng-dev/release/config",
Expand Down
6 changes: 6 additions & 0 deletions ng-dev/release/notes/release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ export class ReleaseNotes {
return render(changelogTemplate, await this.generateRenderContext(), {rmWhitespace: true});
}

/** Retrieve the number of commits included in the release notes after filtering and deduping. */
async getCommitCountInReleaseNotes() {
josephperrott marked this conversation as resolved.
Show resolved Hide resolved
const context = await this.generateRenderContext();
return context.commits.filter(context.includeInReleaseNotes()).length;
}

/**
* Gets the URL fragment for the release notes. The URL fragment identifier
* can be used to point to a specific changelog entry through an URL.
Expand Down
14 changes: 14 additions & 0 deletions ng-dev/release/publish/test/release-notes/generation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,18 @@ describe('release notes generation', () => {
`);
});
});

it('should determine the number of commits included in the entry', async () => {
SandboxGitRepo.withInitialCommit(githubConfig)
.createTagForHead('0.0.0')
.commit('fix: first thing fixed')
.commit('feat: first new thing')
.commit('feat: second new thing')
.commit('build: rework everything')
.commit('fix: fix what we broke');

const releaseNotes = await ReleaseNotes.forRange(parse('0.0.1'), '0.0.0', 'HEAD');

expect(await releaseNotes.getCommitCountInReleaseNotes()).toBe(4);
});
});
1 change: 1 addition & 0 deletions ng-dev/utils/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ ts_library(
visibility = [
"//github-actions/slash-commands/lib:__subpackages__",
"//ng-dev:__subpackages__",
"//tools/local-actions/changelog/lib:__subpackages__",
],
deps = [
"@npm//@octokit/core",
Expand Down
8 changes: 8 additions & 0 deletions ng-dev/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ const USER_CONFIG_FILE_PATH = '.ng-dev.user';
/** The local user configuration for ng-dev. */
let userConfig: {[key: string]: any} | null = null;

/**
* Set the cached configuration object to be loaded later. Only to be used on CI situations in
* which loading from the `.ng-dev/` directory is not possible.
*/
export function setConfig(config: {}) {
cachedConfig = config;
}

/**
* Get the configuration from the file system, returning the already loaded
* copy if it is defined.
Expand Down
19 changes: 0 additions & 19 deletions ng-dev/utils/git/authenticated-git-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,25 +127,6 @@ export class AuthenticatedGitClient extends GitClient {
'Unable to configure `AuthenticatedGitClient` as it has been configured already.',
);
}
if (process.env['GITHUB_ACTIONS']) {
throw Error(
'Cannot use `configure` static method to create AuthenticatedGitClient in a Github Action.',
);
}
AuthenticatedGitClient._authenticatedInstance = new AuthenticatedGitClient(token);
}

/** Configures an authenticated git client in the context of a Github action. */
static configureForGithubActions(token: string, config: {github: GithubConfig}): void {
if (AuthenticatedGitClient._authenticatedInstance) {
throw Error(
'Unable to configure `AuthenticatedGitClient` as it has been configured already.',
);
}
AuthenticatedGitClient._authenticatedInstance = new AuthenticatedGitClient(
token,
undefined,
config,
);
}
}
19 changes: 19 additions & 0 deletions tools/local-actions/changelog/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "esbuild_checked_in")

esbuild_checked_in(
name = "post",
entry_point = "//tools/local-actions/changelog/lib:post.ts",
external = ["ts-node"],
deps = [
"//tools/local-actions/changelog/lib",
],
)

esbuild_checked_in(
name = "main",
entry_point = "//tools/local-actions/changelog/lib:main.ts",
external = ["ts-node"],
deps = [
"//tools/local-actions/changelog/lib",
],
)
11 changes: 11 additions & 0 deletions tools/local-actions/changelog/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
name: 'Create DevInfra Changelogs'
description: 'Automatically create changelog entries for the dev-infra repo'
author: 'Angular'
inputs:
angular-robot-key:
description: 'The private key for the Angular Robot Github app.'
required: true
runs:
using: 'node12'
main: 'main.js'
post: 'post.js'
28 changes: 28 additions & 0 deletions tools/local-actions/changelog/lib/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//tools/local-actions/changelog:__subpackages__"])

exports_files([
"main.ts",
"post.ts",
])

ts_library(
name = "lib",
srcs = glob(
["*.ts"],
exclude = ["*.spec.ts"],
),
deps = [
"//github-actions:utils",
"//ng-dev/release/config",
"//ng-dev/release/notes",
"//ng-dev/utils",
"@npm//@actions/core",
"@npm//@actions/github",
"@npm//@octokit/rest",
"@npm//@types/node",
"@npm//@types/semver",
"@npm//semver",
],
)
132 changes: 132 additions & 0 deletions tools/local-actions/changelog/lib/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as core from '@actions/core';
import {context} from '@actions/github';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {join} from 'path';
import {SemVer} from 'semver';
import {ReleaseNotes} from '../../../../ng-dev/release/notes/release-notes';
import {AuthenticatedGitClient} from '../../../../ng-dev/utils/git/authenticated-git-client';
import {ANGULAR_ROBOT, getAuthTokenFor} from '../../../../github-actions/utils';
import {GithubConfig, setConfig} from '../../../../ng-dev/utils/config';
import {ReleaseConfig} from '../../../../ng-dev/release/config/index';

/** The tag used for tracking the last time the changlog was generated. */
const lastChangelogTag = 'most-recent-changelog-generation';
/** Marker comment used to split the changelog into a list of distinct changelog entries. */
const splitMarker = '\n<!-- CHANGELOG SPLIT MARKER -->\n';
/** The commit message used for the changes to the CHANGELOG. */
const commitMessage = 'release: create weekly changelog entry';

// Set the cached configuration object to be used throughout the action.
const config: {github: GithubConfig; release: ReleaseConfig} = {
github: {
mainBranchName: 'main',
name: context.repo.repo,
owner: context.repo.owner,
},
release: {
npmPackages: [],
buildPackages: async () => [],
releaseNotes: {
categorizeCommit: (commit) => {
const [groupName, area] = commit.scope.split('/');
/** The scope slug to be used in the description's used in CHANGELOG.md */
const scope = area ? `**${area}:** ` : '';
return {
groupName,
description: `${scope}${commit.subject}`,
};
},
},
},
};
setConfig(config);
josephperrott marked this conversation as resolved.
Show resolved Hide resolved

async function run(): Promise<void> {
// Configure the AuthenticatedGitClient to be authenticated with the token for the Angular Robot.
AuthenticatedGitClient.configure(await getAuthTokenFor(ANGULAR_ROBOT));
/** The authenticed GitClient. */
const git = AuthenticatedGitClient.get();
git.run(['config', 'user.email', '[email protected]']);
git.run(['config', 'user.name', 'Angular Robot']);

/** The full path to the changelog file. */
const changelogFile = join(git.baseDir, 'CHANGELOG.md');
/** The full path of the changelog */
const changelogArchiveFile = join(git.baseDir, 'CHANGELOG_ARCHIVE.md');
/** The sha of the commit when the changelog was most recently generated. */
const lastChangelogRef = getLatestRefFromUpstream(lastChangelogTag);
/** The sha of the latest commit on the main branch. */
const latestRef = getLatestRefFromUpstream(git.mainBranchName);
/** The release notes generation object. */
const releaseNotes = await ReleaseNotes.forRange(getTodayAsSemver(), lastChangelogRef, latestRef);

if ((await releaseNotes.getCommitCountInReleaseNotes()) === 0) {
console.log('No release notes are needed as no commits would be included.');
return;
}

/** The changelog entry for commits on the main branch since the last changelog was generated. */
const changelogEntry = await releaseNotes.getChangelogEntry();

// Checkout the main branch at the latest commit.
git.run(['checkout', '--detach', latestRef]);

/** The changelog entries in the current changelog. */
const changelog = readFileSync(changelogFile, {encoding: 'utf8'}).split(splitMarker);

// When the changelog has more than 12 entries (roughly one quarter of the year in weekly
// releases), extra changelog entries are moved to the changelog archive.
if (changelog.length > 12) {
/** The changelog entries in the changelog archive. */
let changelogArchive: string[] = [];
if (existsSync(changelogArchiveFile)) {
changelogArchive = readFileSync(changelogArchiveFile, {encoding: 'utf8'}).split(splitMarker);
}
changelogArchive.unshift(...changelog.splice(12));
writeAndAddToGit(changelogArchiveFile, changelogArchive.join(splitMarker));
}

// Place the new changelog entry at the beginning of the changelog entries list.
changelog.unshift(changelogEntry);
writeAndAddToGit(changelogFile, changelog.join(splitMarker));

// Commit the new changelog(s) and push the changes to github.
git.run(['commit', '--no-verify', '-m', commitMessage]);
git.run(['push', git.getRepoGitUrl(), `HEAD:refs/heads/${git.mainBranchName}`]);
// A force push is used to update the tag git does not expect it to move and a force is neccessary
// to update it from its old sha.
git.run(['push', '-f', git.getRepoGitUrl(), `HEAD:refs/tags/${lastChangelogTag}`]);
}

/** Write the contents to the provided file and add it to git staging. */
function writeAndAddToGit(filePath: string, contents: string) {
const git = AuthenticatedGitClient.get();
writeFileSync(filePath, contents);
git.run(['add', filePath]);
}

/** Retrieve the latest ref for the branch or tag from upstream. */
function getLatestRefFromUpstream(branchOrTag: string) {
try {
const git = AuthenticatedGitClient.get();
git.runGraceful(['fetch', git.getRepoGitUrl(), branchOrTag, '--depth=250']);
return git.runGraceful(['rev-parse', 'FETCH_HEAD']).stdout.trim();
} catch {
core.error(`Unable to retrieve '${branchOrTag}' from upstream`);
process.exit(1);
}
}

/** Create a semver tag based on todays date. */
function getTodayAsSemver() {
const today = new Date();
return new SemVer(`${today.getFullYear()}.${today.getMonth() + 1}.${today.getDay()}`);
}

// This action should only be run in the angular/dev-infra repo.
if (context.repo.owner === 'angular' && context.repo.repo === 'dev-infra') {
run().catch((e: Error) => {
core.error(e);
core.setFailed(e.message);
});
}
7 changes: 7 additions & 0 deletions tools/local-actions/changelog/lib/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {revokeAuthTokenFor, ANGULAR_ROBOT} from '../../../../github-actions/utils';

async function run(): Promise<void> {
await revokeAuthTokenFor(ANGULAR_ROBOT);
}

run();
Loading