Skip to content

Commit

Permalink
[#38] Add Changelog generator
Browse files Browse the repository at this point in the history
  • Loading branch information
zero88 committed Aug 23, 2022
1 parent ae681da commit fd213b5
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 6 deletions.
19 changes: 19 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,25 @@ inputs:
description: 'CI: Dry run. If `true`, action will run without do modify files or git commit/tag'
required: true
default: 'false'
changelog:
description: 'Enable generate CHANGELOG'
required: false
default: 'false'
changelogImageTag:
description: 'CHANGELOG docker image tag: https://github.com/github-changelog-generator/docker-github-changelog-generator'
required: false
default: '1.16.2'
changelogConfigFile:
description: 'CHANGELOG config file: https://github.com/github-changelog-generator/github-changelog-generator#params-file'
required: false
default: '.github_changelog_generator'
changelogToken:
description: 'CHANGELOG token to query GitHub API: https://github.com/github-changelog-generator/github-changelog-generator#github-token'
required: false
changelogMsg:
description: 'CI: Changelog generator commit message template'
required: false
default: Generated CHANGELOG
outputs:
branch:
description: 'Current branch name or tag name'
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

109 changes: 109 additions & 0 deletions src/changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import path from 'path';
import { replaceInFile } from 'replace-in-file';
import { DockerRunCLI } from './docker';
import { readEnv } from './exec';
import { CommitStatus } from './gitOps';
import { isEmpty, RegexUtils, removeEmptyProperties } from './utils';

export interface ChangelogConfig {
/**
* Check whether changelog generator is active or not
* <p>
* Default value is `false`
* @type {boolean}
*/
readonly active: boolean;
/**
* Returns changelog-generator docker image
* {@link https://hub.docker.com/r/githubchangeloggenerator/github-changelog-generator/}
*/
readonly image: string;
/**
* Returns changelog-generator config file
* {@link https://github.com/github-changelog-generator/github-changelog-generator#params-file}
*/
readonly configFile: string;
/**
* Returns changelog-generator token to query GitHub API:
* {@link https://github.com/github-changelog-generator/github-changelog-generator#github-token}
*/
readonly commitMsg: string;
/**
* Returns changelog-generator token to query GitHub API:
* {@link https://github.com/github-changelog-generator/github-changelog-generator#github-token}
*/
readonly token?: string;
}

export type ChangelogResult = {
readonly generated: boolean;
readonly latestTag: string;
readonly releaseTag: string;
} & Omit<CommitStatus, 'isPushed'>

const getTag = (tag?: string) => isEmpty(tag) ? '1.16.2' : tag;
const getImage = (tag?: string) => `githubchangeloggenerator/github-changelog-generator:${getTag(tag)}`;

const defaultConfig: ChangelogConfig = {
active: false,
image: getImage(),
configFile: '.github_changelog_generator',
commitMsg: 'Generated CHANGELOG',
};

export const createChangelogConfig = (active?: boolean, imageTag?: string, configFile?: string, token?: string,
commitMsg?: string): ChangelogConfig => {
return {
...defaultConfig, ...removeEmptyProperties({ active, configFile, token, commitMsg, image: getImage(imageTag) }),
};
};

export type GenerateResult = Required<Omit<ChangelogResult, 'commitId' | 'isCommitted'>>;

export class ChangelogOps {
private readonly config: ChangelogConfig;

constructor(config: ChangelogConfig) {
this.config = config;
}

async generate(latestTag: string, releaseTag: string, dryRun: boolean): Promise<GenerateResult> {
const commitMsg = `${this.config.commitMsg} ${releaseTag}`;
const isExisted = await this.verifyExists(releaseTag);
if (isExisted) {
return { latestTag, releaseTag, commitMsg, generated: false };
}
const workspace = readEnv('GITHUB_WORKSPACE');
const owner = readEnv('GITHUB_REPOSITORY_OWNER');
const repo = readEnv('GITHUB_REPOSITORY');
const ghApi = readEnv('GITHUB_API_URL');
const ghSite = readEnv('GITHUB_SERVER_URL');
const project = repo.replace(owner + '/', '');
const cmd = [
`--user`, owner, `--project`, project, `--config-file`, this.config.configFile,
`--since-tag`, latestTag, `--future-release`, releaseTag,
`--github-api`, ghApi, `--github-site`, ghSite,
];
const envs = { 'CHANGELOG_GITHUB_TOKEN': this.config.token };
const volumes = { [workspace]: DockerRunCLI.DEFAULT_WORKDIR };
const dockerRun = await DockerRunCLI.execute(this.config.image, cmd, envs, volumes);
return { latestTag, releaseTag, commitMsg, generated: dockerRun.success };
}

async verifyExists(releaseTag: string): Promise<boolean> {
const re = /((base|output)\s?=\s?)(.+)/;
const result: string[] = [];
await replaceInFile({
files: this.config.configFile, from: new RegExp(re.source, 'gm'), dry: true, allowEmptyPaths: true,
to: match => {
result.push(RegexUtils.searchMatch(match, re, 2));
return match;
},
});
const dir = path.dirname(this.config.configFile);
const files = (isEmpty(result) ? ['CHANGELOG.md'] : result).map(f => path.resolve(dir, f));
return await replaceInFile(
{ files, from: releaseTag, dry: true, countMatches: true, allowEmptyPaths: true, to: match => match })
.then(rr => rr.some(r => r?.numMatches! > 0));
}
}
33 changes: 33 additions & 0 deletions src/docker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as core from '@actions/core';
import { ExecResult, strictExec } from './exec';
import { arrayToObjectWithVal, isNull } from './utils';

export class DockerRunCLI {
static DEFAULT_WORKDIR = '/github/workspace';

static envs = (envs?: Record<string, any>): string[] => {
const def = ['CI', 'GITHUB_API_URL', 'GITHUB_SERVER_URL', 'GITHUB_WORKSPACE'];
const environments = { ...arrayToObjectWithVal(def, () => null), ...(envs ?? {}) };
const toEnv = kv => isNull(kv[1]) ? kv[0] : `${kv[0]}=${kv[1]}`;
return Object.entries(environments).map(toEnv).map(e => ['-e', e]).flat();
};

static volumes = (volumes?: Record<string, any>): string[] => {
const toVolume = kv => isNull(kv[1]) ? kv[0] : `${kv[0]}:${kv[1]}`;
return Object.entries(volumes ?? {}).map(toVolume).map(v => ['-v', v]).flat();
};

static execute = (image: string, containerCmd?: Array<string>, envs?: Record<string, any>,
volumes?: Record<string, any>): Promise<ExecResult> => {
const args = [
'run', '-t', '--rm',
'--workdir', DockerRunCLI.DEFAULT_WORKDIR,
...DockerRunCLI.envs(envs),
...DockerRunCLI.volumes(volumes),
image,
...(containerCmd ?? []),
];
core.debug(`Docker run args: ${args}`);
return strictExec('docker', args, 'Unable to run docker', false);
};
}
2 changes: 2 additions & 0 deletions src/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ export const strictExec = async (command: string, args: string[], msgIfError: st
return r;
});
};

export const readEnv = (envVar: string): string => process.env[envVar] ?? '';
13 changes: 13 additions & 0 deletions src/gitOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ export class GitOps {

static removeRemoteBranch = async (branch: string) => GitOps.execSilent(['push', 'origin', `:${branch}`]);

// git tag -l --sort=-creatordate | head -n 1
static getLatestTag = async (pattern?: string) =>
GitOps.execSilent(['fetch', '--tag'])
.then(
() => strictExec('git', ['tag', '--sort=-creatordate'],
'Cannot get tag', false))
.then(r => {
core.debug(r.stdout);
core.debug(r.stderr);
return r;
})
.then(r => r.stdout.split('\r\n')[0]);

async commit(msg: string, branch?: string): Promise<CommitStatus> {
return this.doCommit(msg, msg, branch);
}
Expand Down
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import { Context } from '@actions/github/lib/context';
import { createChangelogConfig } from './changelog';
import { createGitOpsConfig } from './gitOps';
import { createGitParserConfig } from './gitParser';
import { ProjectContext } from './projectContext';
Expand Down Expand Up @@ -52,7 +53,13 @@ const run = (ghContext: Context) => {
getInputBool('mustSign'),
getInputString('nextVerMsg'));
const dryRun = getInputBool('dry');
const ops = new ProjectOps({ gitParserConfig, versionStrategy, gitOpsConfig });
const changelogConfig = createChangelogConfig(
getInputBool('changelog', false),
getInputString('changelogImageTag', false),
getInputString('changelogConfigFile', false),
getInputString('changelogToken', false),
getInputString('changelogMsg', false));
const ops = new ProjectOps({ gitParserConfig, versionStrategy, gitOpsConfig, changelogConfig });
core.group('Processing...', () => ops.process(ghContext, dryRun))
.then(ghOutput => setActionOutput(ghOutput))
.catch(error => core.setFailed(error));
Expand Down
7 changes: 7 additions & 0 deletions src/projectContext.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ChangelogResult } from './changelog';

export interface Decision {
/**
* Should run the next step: such as build & test
Expand Down Expand Up @@ -38,6 +40,11 @@ export interface CIContext {
* CI auto commit message
*/
readonly commitMsg?: string;

/**
* CI changelog result
*/
readonly changelogResult?: ChangelogResult;
}

export interface Versions {
Expand Down
25 changes: 22 additions & 3 deletions src/projectOps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as core from '@actions/core';
import { Context } from '@actions/github/lib/context';
import { ChangelogConfig, ChangelogOps, ChangelogResult } from './changelog';
import { GitOps, GitOpsConfig } from './gitOps';
import { GitParser, GitParserConfig } from './gitParser';
import { CIContext, Decision, ProjectContext, Versions } from './projectContext';
Expand All @@ -13,6 +14,7 @@ export type ProjectConfig = {
readonly gitParserConfig: GitParserConfig;
readonly gitOpsConfig: GitOpsConfig;
readonly versionStrategy: VersionStrategy;
readonly changelogConfig: ChangelogConfig;

}

Expand All @@ -21,11 +23,13 @@ export class ProjectOps {
readonly projectConfig: ProjectConfig;
private readonly gitParser: GitParser;
private readonly gitOps: GitOps;
private readonly changelogOps: ChangelogOps;

constructor(projectConfig: ProjectConfig) {
this.projectConfig = projectConfig;
this.gitParser = new GitParser(this.projectConfig.gitParserConfig);
this.gitOps = new GitOps(this.projectConfig.gitOpsConfig);
this.changelogOps = new ChangelogOps(projectConfig.changelogConfig);
}

private static makeDecision = (context: RuntimeContext, ci: CIContext): Decision => {
Expand Down Expand Up @@ -64,7 +68,8 @@ export class ProjectOps {

private async fixVersion(expectedVersion: string, dryRun: boolean): Promise<VersionResult> {
core.info(`Fixing version to ${expectedVersion}...`);
const r = await VersionPatternParser.replace(this.projectConfig.versionStrategy.versionPatterns, expectedVersion, dryRun);
const r = await VersionPatternParser.replace(this.projectConfig.versionStrategy.versionPatterns, expectedVersion,
dryRun);
core.info(`Fixed ${r.files?.length} file(s): [${r.files}]`);
return r;
}
Expand Down Expand Up @@ -93,8 +98,12 @@ export class ProjectOps {
if (ctx.isTag) {
throw `Git tag version doesn't meet with current version in files. Invalid files: [${vr.files}]`;
}
ci = await this.gitOps.commitVersionCorrection(ctx.branch, version)
.then(s => this.gitOps.pushCommit(s, dryRun));
const commitVersionStatus = await this.gitOps.commitVersionCorrection(ctx.branch, version);
const changelogResult = await this.generateChangelog(version, dryRun);

if (commitVersionStatus.isCommitted || changelogResult.isCommitted) {
ci = { ...(await this.gitOps.pushCommit({ ...commitVersionStatus }, dryRun)), changelogResult };
}
}
if (ctx.isReleasePR && ctx.isMerged) {
const tag = `${this.projectConfig.gitParserConfig.tagPrefix}${version}`;
Expand Down Expand Up @@ -125,5 +134,15 @@ export class ProjectOps {
}
return { isPushed: false };
}

private async generateChangelog(version: string, dryRun: boolean): Promise<ChangelogResult> {
const tagPrefix = this.projectConfig.gitParserConfig.tagPrefix;
const latestTag = await GitOps.getLatestTag(tagPrefix);
const result = await this.changelogOps.generate(latestTag, tagPrefix + version, dryRun);
if (result.generated) {
return { ...result, ...(await this.gitOps.commit(result.commitMsg)) };
}
return { ...result, isCommitted: false };
}
}

0 comments on commit fd213b5

Please sign in to comment.