Skip to content

Commit

Permalink
Add support for mono-repositories
Browse files Browse the repository at this point in the history
  • Loading branch information
satazor committed Oct 29, 2023
1 parent 2870c47 commit fa9fdff
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 28 deletions.
34 changes: 23 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# github-changelog-generator
Generate changelog files from the project's GitHub PRs

Generate changelog files from the project's GitHub PRs.

## Usage

Generate a new [GitHub Personal Access Token](https://github.com/settings/tokens) and save it to your `.zshrc.local`, `.bashrc.local` or similar:

```sh
Expand All @@ -19,14 +21,16 @@ $ github-changelog-generator --help

Options:

-h, --help output usage information
-b, --base-branch <name> [optional] specify the base branch name - master by default
-f, --future-release <version> [optional] specify the next release version
-t, --future-release-tag <name> [optional] specify the next release tag name if it is different from the release version
-l, --labels <names> [optional] labels to filter pull requests by
-o, --owner <name> [optional] owner of the repository
-r, --repo <name> [optional] name of the repository
--rebuild rebuild the full changelog
-h, --help output usage information
-b, --base-branch <name> [optional] specify the base branch name - master by default
-f, --future-release <version> [optional] specify the next release version
-t, --future-release-tag <name> [optional] specify the next release tag name if it is different from the release version
-rtp, --release-tag-prefix [optional] prefix of release tag when finding the latest release, useful for monorepos
-ctp, --changed-files-prefix [optional] prefix of changed files when finding pull-requests, useful for monorepos
-l, --labels <names> [optional] labels to filter pull requests by
-o, --owner <name> [optional] owner of the repository
-r, --repo <name> [optional] name of the repository
--rebuild rebuild the full changelog
```
To generate a changelog for your GitHub project, use the following command:
Expand All @@ -43,14 +47,22 @@ Example:
$ echo "$(github-changelog-generator --base-branch=production)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md
```
The `--future-release` and `--future-release-tag` options are optional. If you just want to build a new changelog without a new release, you can skip those options, and `github-changelog-generator` will create a changelog for existing releases only. Also, if your future release tag name is the same as your future release version number, then you can skip `--future-release-tag`.
The `--future-release` and `--future-release-tag` options are optional. If your future release tag name is the same as your future release version number, then you can skip `--future-release-tag`.
Example:
```sh
$ echo "$(github-changelog-generator --future-release=1.2.3 --future-release-tag=v1.2.3)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md
```
If you are on a mono-repository, you will need to use `--release-tag-prefix` in order to filter release tags of the package you are targeting. There's also the `--changed-files-prefix` option that may be used to specify the base of the package. However, this should be automatic in most cases, except when we are unable to infer the root folder.
Example:
```sh
$ echo "$(github-changelog-generator --future-release=my-package@1.2.3 --future-release-tag=my-package@v1.2.3 --release-tag-prefix=my-package@v)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md
```
The `--owner` and `--repo` options allow you to specify the owner and name of the GitHub repository, respectively. If omitted, they will default to the values found in the project's git config.
Example:
Expand All @@ -67,7 +79,7 @@ Example:
$ echo "$(github-changelog-generator --labels projectX,general)\n$(tail -n +2 CHANGELOG.md)" > CHANGELOG.md
```
The `--rebuild` option allows you to fetch the repository's full changelog history.
The `--rebuild` option allows you to fetch the repository's full changelog history.
Starting on major version 2, the default behavior for the generator is to only create the changelog for the pull requests that come after the latest release,
so this option allows for backwards compatibility.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@octokit/graphql": "^4.8.0",
"commander": "^8.3.0",
"ini": "^2.0.0",
"look-it-up": "^2.1.0",
"moment": "^2.29.1"
},
"devDependencies": {
Expand Down
90 changes: 80 additions & 10 deletions src/changelog-fetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ class ChangelogFetcher {
* Constructor.
*/

constructor({ base, futureRelease, futureReleaseTag, labels, owner, repo, token }) {
constructor({
base,
changedFilesPrefix,
futureRelease,
futureReleaseTag,
labels,
owner,
repo,
token,
releaseTagPrefix
}) {
this.base = base;
this.changedFilesPrefix = changedFilesPrefix;
this.futureRelease = futureRelease;
this.futureReleaseTag = futureReleaseTag || futureRelease;
this.releaseTagPrefix = releaseTagPrefix;
this.labels = labels || [];
this.owner = owner;
this.repo = repo;
Expand Down Expand Up @@ -75,7 +87,9 @@ class ChangelogFetcher {
}

// Get the latest release.
const latestRelease = await this.getLatestRelease();
const latestRelease = this.releaseTagPrefix
? await this.getLatestReleaseByTagPrefix()
: await this.getLatestRelease();

if (latestRelease.tagName === this.futureReleaseTag) {
throw new Error('Changelog already on the latest release');
Expand All @@ -101,13 +115,15 @@ class ChangelogFetcher {
*/
async getLatestRelease() {
const query = `
query latestRelease($owner: String!, $repo: String!) {
query getLatestRelease($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo){
latestRelease {
name,
tagCommit {
committedDate
}
tagName
url
}
createdAt
}
Expand All @@ -129,7 +145,37 @@ class ChangelogFetcher {
}

/**
* Auxiliary function to iterage through list of PRs.
* Get the latest release by tag prefix.
*
* @return {Object} release - the latest release
*/
async getLatestReleaseByTagPrefix() {
let cursor = '';
let hasMoreResults = true;
let releases = [];
let matchingRelease;

do {
({ releases, cursor, hasMoreResults } = await this.getReleasesQuery(cursor));

matchingRelease = releases[0];
} while (!matchingRelease && hasMoreResults);

// For shiny new repositories without releases, use the repository creation date instead.
if (!matchingRelease) {
const repositoryCreatedAt = await this.getRepositoryCreatedAt();

return { tagCommit: { committedDate: repositoryCreatedAt } };
}

// Transform string timestamp into moment.
matchingRelease.tagCommit.committedDate = moment.utc(matchingRelease.tagCommit.committedDate);

return matchingRelease;
}

/**
* Auxiliary function to iterate through list of PRs.
*
* @param {String} cursor - the cursor from where the function will get the PRs
* @param {Number} pageSize - the number of results we try to fetch each time
Expand All @@ -142,9 +188,16 @@ class ChangelogFetcher {
const [labelsSignature, labelsParam] =
this.labels.length === 0 ? ['', ''] : [', $labels: [String!]', ', labels: $labels'];

const filesFragment = `
files (first: 100) {
nodes {
path
}
}`;

// TODO: replace orderBy from UPDATED_AT to MERGED_AT when available.
const query = `
query pullRequestsBefore($owner: String!, $repo: String!, $first: Int!, $base: String!${cursorSignature}${labelsSignature}) {
query getPullRequests($owner: String!, $repo: String!, $first: Int!, $base: String!${cursorSignature}${labelsSignature}) {
repository(owner: $owner, name: $repo) {
pullRequests(baseRefName: $base, first: $first, orderBy: {field: UPDATED_AT, direction: DESC}, states: [MERGED]${cursorParam}${labelsParam}) {
nodes {
Expand All @@ -154,10 +207,11 @@ class ChangelogFetcher {
updatedAt
url
baseRefName
author{
author {
login
url
}
${this.changedFilesPrefix ? filesFragment : ''}
}
pageInfo {
endCursor
Expand All @@ -175,10 +229,19 @@ class ChangelogFetcher {
repo: this.repo
});

let pullRequests = result.repository.pullRequests.nodes;

if (this.changedFilesPrefix) {
pullRequests = pullRequests
.filter(({ files }) => files.nodes.some(file => file.path.startsWith(this.changedFilesPrefix)))
// eslint-disable-next-line no-unused-vars
.map(({ files, ...rest }) => rest);
}

return {
cursor: result.repository.pullRequests.pageInfo.endCursor,
hasMoreResults: result.repository.pullRequests.pageInfo.hasNextPage,
pullRequests: result.repository.pullRequests.nodes
pullRequests
};
}

Expand Down Expand Up @@ -215,7 +278,7 @@ class ChangelogFetcher {
}

/**
* Auxiliary function to iterage through list of releases.
* Auxiliary function to iterate through list of releases.
*
* @param {String} cursor - the cursor from where the function will get the releases
* @param {Number} pageSize - the number of results we try to fetch each time
Expand All @@ -230,6 +293,7 @@ class ChangelogFetcher {
releases(first: $first, orderBy: {field: CREATED_AT, direction: DESC}${cursorParam}) {
nodes {
name
tagName
tagCommit {
committedDate
}
Expand All @@ -249,10 +313,16 @@ class ChangelogFetcher {
repo: this.repo
});

let releases = result.repository.releases.nodes;

if (this.releaseTagPrefix) {
releases = releases.filter(({ tagName }) => tagName.startsWith(this.releaseTagPrefix));
}

return {
cursor: result.repository.releases.pageInfo.endCursor,
hasMoreResults: result.repository.releases.pageInfo.hasNextPage,
releases: result.repository.releases.nodes
releases
};
}

Expand Down Expand Up @@ -290,7 +360,7 @@ class ChangelogFetcher {
*/
async getRepositoryCreatedAt() {
const query = `
query repositoryCreation($owner: String!, $repo: String!) {
query getRepositoryCreatedAt($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
createdAt
}
Expand Down
40 changes: 33 additions & 7 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

const { Command } = require('commander');
const { formatChangelog } = require('./changelog-formatter');
const { lookItUpSync } = require('look-it-up');
const { readFileSync } = require('fs');
const ChangelogFetcher = require('./changelog-fetcher');
const ini = require('ini');
Expand All @@ -28,6 +29,14 @@ program
'-t, --future-release-tag <name>',
'[optional] specify the next release tag name if it is different from the release version'
)
.option(
'-rtp, --release-tag-prefix <prefix>',
'[optional] prefix of release tag when finding the latest release, useful for monorepos'
)
.option(
'-rfp, --changed-files-prefix <prefix>',
'[optional] changed files prefix to filter pull requests by, useful for monorepos'
)
.option('-l, --labels <names>', '[optional] labels to filter pull requests by', val => val.split(','))
.option('-o, --owner <name>', '[optional] owner of the repository')
.option('-r, --repo <name>', '[optional] name of the repository')
Expand All @@ -40,20 +49,18 @@ program
*/

const options = program.opts();
const base = options.baseBranch || 'master';
const { futureRelease, futureReleaseTag, labels, rebuild } = options;
const gitDir = lookItUpSync('.git');
const { baseBranch = 'master', futureRelease, futureReleaseTag, labels, rebuild, releaseTagPrefix } = options;
const token = process.env.GITHUB_TOKEN;
let { owner, repo } = options;
let { owner, repo, changedFilesPrefix } = options;

/**
* Infer owner and repo from git config if not provided.
*/

if (!owner || !repo) {
const dir = path.resolve('.');

try {
const gitconfig = readFileSync(path.join(dir, '.git/config'), 'utf-8');
const gitconfig = readFileSync(path.join(gitDir, 'config'), 'utf-8');
const remoteOrigin = ini.parse(gitconfig)['remote "origin"'];
const match = remoteOrigin.url.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/);

Expand All @@ -69,12 +76,31 @@ if (!owner || !repo) {
}
}

/**
* Infer changed files prefix from git directory.
*/

if (!changedFilesPrefix) {
changedFilesPrefix = path.relative(path.resolve(gitDir, '..'), '.').split(path.sep).join(path.posix.sep);
}

/**
* Run the changelog generator.
*/

async function run() {
const fetcher = new ChangelogFetcher({ base, futureRelease, futureReleaseTag, labels, owner, repo, token });
const fetcher = new ChangelogFetcher({
base: baseBranch,
changedFilesPrefix,
futureRelease,
futureReleaseTag,
labels,
owner,
releaseTagPrefix,
repo,
token
});

const releases = await (rebuild ? fetcher.fetchFullChangelog() : fetcher.fetchLatestChangelog());

formatChangelog(releases).forEach(line => process.stdout.write(line));
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2216,6 +2216,11 @@ lodash@^4.17.21:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==

look-it-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/look-it-up/-/look-it-up-2.1.0.tgz#278a7ffc9da60a928452a0bab5452bb8855d7d13"
integrity sha512-nMoGWW2HurtuJf6XAL56FWTDCWLOTSsanrgwOyaR5Y4e3zfG5N/0cU5xWZSEU3tBxhQugRbV1xL9jb+ug7yZww==

lru-cache@^4.0.1:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
Expand Down

0 comments on commit fa9fdff

Please sign in to comment.