Skip to content

feat: add the ability to create a PR with the incoming changelog #110

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

Open
wants to merge 2 commits into
base: main
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
109 changes: 12 additions & 97 deletions src/commands/github.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { promises as fsp } from "node:fs";
import consola from "consola";
import type { Argv } from "mri";
import { resolve } from "pathe";
import consola from "consola";
import { underline, cyan } from "colorette";
import {
getGithubChangelog,
resolveGithubToken,
syncGithubRelease,
} from "../github";
import {
ChangelogConfig,
loadChangelogConfig,
parseChangelogMarkdown,
loadChangelogConfig
} from "..";
import { pullRequest } from "./github/pull-request";
import { release } from "./github/release";

const availableCommands = new Set(["release", "pull-request"]);

export default async function githubMain(args: Argv) {
const cwd = resolve(args.dir || "");
process.chdir(cwd);

const [subCommand, ..._versions] = args._;
if (subCommand !== "release") {
if (!availableCommands.has(subCommand)) {
consola.log(
"Usage: changelogen gh release [all|versions...] [--dir] [--token]"
);
Expand All @@ -30,7 +25,7 @@ export default async function githubMain(args: Argv) {

if (config.repo?.provider !== "github") {
consola.error(
"This command is only supported for github repository provider."
"These command are only supported for github repository provider."
);
process.exit(1);
}
Expand All @@ -39,89 +34,9 @@ export default async function githubMain(args: Argv) {
config.tokens.github = args.token;
}

let changelogMd: string;
if (typeof config.output === "string") {
changelogMd = await fsp
.readFile(resolve(config.output), "utf8")
.catch(() => null);
}
if (!changelogMd) {
changelogMd = await getGithubChangelog(config).catch(() => null);
}
if (!changelogMd) {
consola.error(`Cannot resolve CHANGELOG.md`);
process.exit(1);
}

const changelogReleases = parseChangelogMarkdown(changelogMd).releases;

let versions = [..._versions].map((v) => v.replace(/^v/, ""));
if (versions[0] === "all") {
versions = changelogReleases.map((r) => r.version).sort();
} else if (versions.length === 0) {
if (config.newVersion) {
versions = [config.newVersion];
} else if (changelogReleases.length > 0) {
versions = [changelogReleases[0].version];
}
}

if (versions.length === 0) {
consola.error(`No versions specified to release!`);
process.exit(1);
}

for (const version of versions) {
const release = changelogReleases.find((r) => r.version === version);
if (!release) {
consola.warn(
`No matching changelog entry found for ${version} in CHANGELOG.md. Skipping!`
);
continue;
}
if (!release.body || !release.version) {
consola.warn(
`Changelog entry for ${version} in CHANGELOG.md is missing body or version. Skipping!`
);
continue;
}
await githubRelease(config, {
version: release.version,
body: release.body,
});
}
}

export async function githubRelease(
config: ChangelogConfig,
release: { version: string; body: string }
) {
if (!config.tokens.github) {
config.tokens.github = await resolveGithubToken(config).catch(
() => undefined
);
}
const result = await syncGithubRelease(config, release);
if (result.status === "manual") {
if (result.error) {
consola.error(result.error);
process.exitCode = 1;
}
const open = await import("open").then((r) => r.default);
await open(result.url)
.then(() => {
consola.info(`Followup in the browser to manually create the release.`);
})
.catch(() => {
consola.info(
`Open this link to manually create a release: \n` +
underline(cyan(result.url)) +
"\n"
);
});
} else {
consola.success(
`Synced ${cyan(`v${release.version}`)} to Github releases!`
);
if (subCommand === "release") {
await release(args, config);
} else if (subCommand === "pull-request") {
await pullRequest(args, config);
}
}
74 changes: 74 additions & 0 deletions src/commands/github/issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Argv } from "mri";
import consola from "consola";
import { ChangelogConfig } from "../../config";
import { getGitDiff, parseCommits } from "../../git";
import { bumpVersion } from "../../semver";
import { generateMarkDown } from "../../markdown";

/**
* This command will:
* - Get the new version
* - Create a new issue with the changelog
*/
export async function issue(args: Argv, config: ChangelogConfig) {
// Get raw commits from the last release
const rawCommits = await getGitDiff(config.from, config.to);

// Parse commits as conventional commits in order to get the new version
const commits = parseCommits(rawCommits, config).filter(
(c) =>
config.types[c.type] &&
!(c.type === "chore" && c.scope === "deps" && !c.isBreaking)
);

// Get the new version
const bumpOptions = _getBumpVersionOptions(args);
// TODO: create a new function to only get the new version (using semver.inc without bumping package.json)
const newVersion = await bumpVersion(commits, config, bumpOptions);
if (!newVersion) {
consola.error("Unable to bump version based on changes.");
process.exit(1);
}
config.newVersion = newVersion;

// Generate changelog
// TODO: Add a template for the markdown issue body
const markdown = await generateMarkDown(commits, config);

const [currentIssue] = await getGithubIssue(config);

if (currentIssue) {
await updateGithubIssue(config, currentIssue, markdown);
}

// TODO: Add a type for the issue body
const body = {}

await createGithubIssue(config, body);
}

// Duplicated from ./src/commands/default.ts. Can we create a shared function?
function _getBumpVersionOptions(args: Argv): BumpVersionOptions {
for (const type of [
"major",
"premajor",
"minor",
"preminor",
"patch",
"prepatch",
"prerelease",
] as const) {
const value = args[type];
if (value) {
if (type.startsWith("pre")) {
return {
type,
preid: typeof value === "string" ? value : "",
};
}
return {
type,
};
}
}
}
105 changes: 105 additions & 0 deletions src/commands/github/pull-request.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { execa } from "execa";
import { Argv } from "mri";
import consola from "consola";
import { ChangelogConfig } from "../../config";
import { BumpVersionOptions, bumpVersion } from "../../semver";
import { getGitDiff, parseCommits } from "../../git";
import { generateMarkDown } from "../../markdown";
import { createGithubPullRequest, getGithubPullRequest, updateGithubPullRequest } from "../../github";

/**
* This command will:
* - Bump version
* - Create a new branch
* - Commit the new version
* - Push the new branch
* - Create a pull request with changelog
*/
export async function pullRequest(args: Argv, config: ChangelogConfig) {
const filesToAdd = ["package.json"];

// Get raw commits from the last release
const rawCommits = await getGitDiff(config.from, config.to);

// Parse commits as conventional commits in order to get the new version
const commits = parseCommits(rawCommits, config).filter(
(c) =>
config.types[c.type] &&
!(c.type === "chore" && c.scope === "deps" && !c.isBreaking)
);

// Get the new version
const bumpOptions = _getBumpVersionOptions(args);
const newVersion = await bumpVersion(commits, config, bumpOptions);
if (!newVersion) {
consola.error("Unable to bump version based on changes.");
process.exit(1);
}
config.newVersion = newVersion;

// Create a new branch before committing
await execa("git", ["checkout", "-b", "v" + config.newVersion], { cwd: config.cwd });

// Add updated files to git
await execa("git", ["add", ...filesToAdd], { cwd: config.cwd });
// Commit changes
const msg = config.templates.commitMessage.replaceAll(
"{{newVersion}}",
config.newVersion
);

// TODO: Add a way to configure username and email

await execa("git", ["commit", "-m", msg], { cwd: config.cwd });

// Push branch and changes to remote
// TODO: Add a way to configure remote (useful for forks in dev mode)
await execa("git", ["push", "-u", "--force", "fork", "v" + config.newVersion], { cwd: config.cwd });

// Generate changelog
// TODO: Add a template for the markdown PR body
const markdown = await generateMarkDown(commits, config);

const [currentPR] = await getGithubPullRequest(config);

if (currentPR) {
await updateGithubPullRequest(config, currentPR.number, markdown);
}

// TODO: Add a type for the body
const body = {
title: 'v' + config.newVersion,
head: 'v' + config.newVersion,
base: 'main',
body: markdown,
draft: true,
}

await createGithubPullRequest(config, body);
}

// Duplicated from ./src/commands/default.ts. Can we create a shared function?
function _getBumpVersionOptions(args: Argv): BumpVersionOptions {
for (const type of [
"major",
"premajor",
"minor",
"preminor",
"patch",
"prepatch",
"prerelease",
] as const) {
const value = args[type];
if (value) {
if (type.startsWith("pre")) {
return {
type,
preid: typeof value === "string" ? value : "",
};
}
return {
type,
};
}
}
}
Loading