Fixed an issue with ignored package being included in PR body incorrectly (#38)

This commit is contained in:
Mateusz Burzyński
2020-07-06 19:08:41 +10:00
committed by GitHub
parent c240b08dfb
commit cd904b92c7
22 changed files with 2246 additions and 1654 deletions
+75
View File
@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`version creates simple PR 1`] = `
Array [
Object {
"base": "some-branch",
"body": "This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to some-branch, this PR will be updated.
# Releases
## simple-project-pkg-a@1.1.0
### Minor Changes
- Awesome feature
### Patch Changes
- Updated dependencies [undefined]
- simple-project-pkg-b@1.1.0
## simple-project-pkg-b@1.1.0
### Minor Changes
- Awesome feature
",
"head": "changeset-release/some-branch",
"owner": "changesets",
"repo": "action",
"title": "Version Packages",
},
]
`;
exports[`version doesn't include ignored package that got a dependency update in the PR body 1`] = `
Array [
Object {
"base": "some-branch",
"body": "This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to some-branch, this PR will be updated.
# Releases
## ignored-package-pkg-b@1.1.0
### Minor Changes
- Awesome feature
",
"head": "changeset-release/some-branch",
"owner": "changesets",
"repo": "action",
"title": "Version Packages",
},
]
`;
exports[`version only includes bumped packages in the PR body 1`] = `
Array [
Object {
"base": "some-branch",
"body": "This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing). If you're not ready to do a release yet, that's fine, whenever you add more changesets to some-branch, this PR will be updated.
# Releases
## simple-project-pkg-a@1.1.0
### Minor Changes
- Awesome feature
",
"head": "changeset-release/some-branch",
"owner": "changesets",
"repo": "action",
"title": "Version Packages",
},
]
`;
+45
View File
@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`it sorts the things right 1`] = `
Array [
Object {
"highestLevel": 3,
"name": "c",
"private": false,
},
Object {
"highestLevel": 1,
"name": "b",
"private": false,
},
Object {
"highestLevel": 3,
"name": "a",
"private": true,
},
]
`;
exports[`it works 1`] = `
"### Major Changes
- [2164a779](https://github.com/keystonejs/keystone-5/commit/2164a779):
- Replace jade with pug because Jade was renamed to Pug, and \`jade\` package is outdated
### Patch Changes
- [81dc0be5](https://github.com/keystonejs/keystone-5/commit/81dc0be5):
- Update dependencies
"
`;
exports[`it works 2`] = `
"### Patch Changes
- [19fe6c1b](https://github.com/keystonejs/keystone-5/commit/19fe6c1b):
Move frontmatter in docs into comments
"
`;
+61
View File
@@ -0,0 +1,61 @@
import { exec } from "@actions/exec";
import { execWithOutput } from "./utils";
export const setupUser = async () => {
await exec("git", [
"config",
"--global",
"user.name",
`"github-actions[bot]"`,
]);
await exec("git", [
"config",
"--global",
"user.email",
`"github-actions[bot]@users.noreply.github.com"`,
]);
};
export const pullBranch = async (branch: string) => {
await exec("git", ["pull", "origin", branch]);
};
export const push = async (
branch: string,
{ force, includeTags }: { force?: boolean; includeTags?: boolean } = {}
) => {
await exec(
"git",
[
"push",
"origin",
`HEAD:${branch}`,
includeTags && "--tags",
force && "--force",
].filter<string>(Boolean as any)
);
};
export const switchToMaybeExistingBranch = async (branch: string) => {
let { stderr } = await execWithOutput("git", ["checkout", branch], {
ignoreReturnCode: true,
});
let isCreatingBranch = !stderr
.toString()
.includes(`Switched to a new branch '${branch}'`);
if (isCreatingBranch) {
await exec("git", ["checkout", "-b", branch]);
}
};
export const reset = async (
pathSpec: string,
mode: "hard" | "soft" | "mixed" = "hard"
) => {
await exec("git", ["reset", `--${mode}`, pathSpec]);
};
export const commitAll = async (message: string) => {
await exec("git", ["add", "."]);
await exec("git", ["commit", "-m", message]);
};
+82
View File
@@ -0,0 +1,82 @@
import * as core from "@actions/core";
import fs from "fs-extra";
import * as gitUtils from "./gitUtils";
import { runPublish, runVersion } from "./run";
import readChangesetState from "./readChangesetState";
const getOptionalInput = (name: string) => core.getInput(name) || undefined;
(async () => {
let githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
core.setFailed("Please add the GITHUB_TOKEN to the changesets action");
return;
}
core.setOutput("published", "false");
core.setOutput("publishedPackages", "[]");
console.log("setting git user");
await gitUtils.setupUser();
console.log("setting GitHub credentials");
await fs.writeFile(
`${process.env.HOME}/.netrc`,
`machine github.com\nlogin github-actions[bot]\npassword ${githubToken}`
);
let { changesets } = await readChangesetState();
let publishScript = core.getInput("publish");
let hasChangesets = changesets.length !== 0;
let hasPublishScript = !!publishScript;
switch (true) {
case !hasChangesets && !hasPublishScript:
console.log("No changesets found");
return;
case !hasChangesets && hasPublishScript: {
console.log(
"No changesets found, attempting to publish any unpublished packages to npm"
);
let npmrcPath = `${process.env.HOME}/.npmrc`;
if (fs.existsSync(npmrcPath)) {
console.log("Found existing .npmrc file");
} else {
console.log("No .npmrc file found, creating one");
fs.writeFileSync(
npmrcPath,
`//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}`
);
}
const result = await runPublish({
script: publishScript,
githubToken,
});
if (result.published) {
core.setOutput("published", "true");
core.setOutput(
"publishedPackages",
JSON.stringify(result.publishedPackages)
);
}
return;
}
case hasChangesets:
await runVersion({
script: getOptionalInput("version"),
githubToken,
prTitle: getOptionalInput("title"),
commitMessage: getOptionalInput("commit"),
hasPublishScript,
});
return;
}
})().catch((err) => {
console.error(err);
core.setFailed(err.message);
});
+27
View File
@@ -0,0 +1,27 @@
import { PreState, NewChangeset } from "@changesets/types";
import { readPreState } from "@changesets/pre";
import readChangesets from "@changesets/read";
export type ChangesetState = {
preState: PreState | undefined;
changesets: NewChangeset[];
};
export default async function readChangesetState(
cwd: string = process.cwd()
): Promise<ChangesetState> {
let preState = await readPreState(cwd);
let isInPreMode = preState !== undefined && preState.mode === "pre";
let changesets = await readChangesets(cwd);
if (isInPreMode) {
let changesetsToFilter = new Set(preState.changesets);
changesets = changesets.filter((x) => !changesetsToFilter.has(x.id));
}
return {
preState: isInPreMode ? preState : undefined,
changesets,
};
}
+148
View File
@@ -0,0 +1,148 @@
import fixturez from "fixturez";
import * as github from "@actions/github";
import fs from "fs-extra";
import path from "path";
import writeChangeset from "@changesets/write";
import { Changeset } from "@changesets/types";
import { runVersion } from "./run";
jest.mock("@actions/github", () => ({
context: {
repo: {
owner: "changesets",
repo: "action",
},
ref: "refs/heads/some-branch",
sha: "xeac7",
},
GitHub: jest.fn(),
}));
jest.mock("./gitUtils");
let mockedGithubMethods = {
search: {
issuesAndPullRequests: jest.fn(),
},
pulls: {
create: jest.fn(),
},
repos: {
createRelease: jest.fn(),
},
};
(github.GitHub as any).mockImplementation(() => mockedGithubMethods);
let f = fixturez(__dirname);
const linkNodeModules = async (cwd: string) => {
await fs.symlink(
path.join(__dirname, "..", "node_modules"),
path.join(cwd, "node_modules")
);
};
const writeChangesets = (changesets: Changeset[], cwd: string) => {
return Promise.all(changesets.map((commit) => writeChangeset(commit, cwd)));
};
beforeEach(() => {
jest.clearAllMocks();
});
describe("version", () => {
it("creates simple PR", async () => {
let cwd = f.copy("simple-project");
linkNodeModules(cwd);
mockedGithubMethods.search.issuesAndPullRequests.mockImplementationOnce(
() => ({ data: { items: [] } })
);
await writeChangesets(
[
{
releases: [
{
name: "simple-project-pkg-a",
type: "minor",
},
{
name: "simple-project-pkg-b",
type: "minor",
},
],
summary: "Awesome feature",
},
],
cwd
);
await runVersion({
githubToken: "@@GITHUB_TOKEN",
cwd,
});
expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot();
});
it("only includes bumped packages in the PR body", async () => {
let cwd = f.copy("simple-project");
linkNodeModules(cwd);
mockedGithubMethods.search.issuesAndPullRequests.mockImplementationOnce(
() => ({ data: { items: [] } })
);
await writeChangesets(
[
{
releases: [
{
name: "simple-project-pkg-a",
type: "minor",
},
],
summary: "Awesome feature",
},
],
cwd
);
await runVersion({
githubToken: "@@GITHUB_TOKEN",
cwd,
});
expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot();
});
it("doesn't include ignored package that got a dependency update in the PR body", async () => {
let cwd = f.copy("ignored-package");
linkNodeModules(cwd);
mockedGithubMethods.search.issuesAndPullRequests.mockImplementationOnce(
() => ({ data: { items: [] } })
);
await writeChangesets(
[
{
releases: [
{
name: "ignored-package-pkg-b",
type: "minor",
},
],
summary: "Awesome feature",
},
],
cwd
);
await runVersion({
githubToken: "@@GITHUB_TOKEN",
cwd,
});
expect(mockedGithubMethods.pulls.create.mock.calls[0]).toMatchSnapshot();
});
});
+272
View File
@@ -0,0 +1,272 @@
import { exec } from "@actions/exec";
import * as github from "@actions/github";
import fs from "fs-extra";
import { getPackages, Package } from "@manypkg/get-packages";
import path from "path";
import * as semver from "semver";
import {
getChangelogEntry,
execWithOutput,
getChangedPackages,
sortTheThings,
getVersionsByDirectory,
} from "./utils";
import * as gitUtils from "./gitUtils";
import readChangesetState from "./readChangesetState";
const createRelease = async (
octokit: github.GitHub,
{ pkg, tagName }: { pkg: Package; tagName: string }
) => {
try {
let changelogFileName = path.join(pkg.dir, "CHANGELOG.md");
let changelog = await fs.readFile(changelogFileName, "utf8");
let changelogEntry = getChangelogEntry(changelog, pkg.packageJson.version);
if (!changelogEntry) {
// we can find a changelog but not the entry for this version
// if this is true, something has probably gone wrong
throw new Error(
`Could not find changelog entry for ${pkg.packageJson.name}@${pkg.packageJson.version}`
);
}
await octokit.repos.createRelease({
tag_name: tagName,
body: changelogEntry.content,
prerelease: pkg.packageJson.version.includes("-"),
...github.context.repo,
});
} catch (err) {
// if we can't find a changelog, the user has probably disabled changelogs
if (err.code !== "ENOENT") {
throw err;
}
}
};
type PublishOptions = {
script: string;
githubToken: string;
cwd?: string;
};
type PublishedPackage = { name: string; version: string };
type PublishResult =
| {
published: true;
publishedPackages: PublishedPackage[];
}
| {
published: false;
};
export async function runPublish({
script,
githubToken,
cwd = process.cwd(),
}: PublishOptions): Promise<PublishResult> {
let branch = github.context.ref.replace("refs/heads/", "");
let octokit = new github.GitHub(githubToken);
let [publishCommand, ...publishArgs] = script.split(/\s+/);
let changesetPublishOutput = await execWithOutput(
publishCommand,
publishArgs,
{ cwd }
);
await gitUtils.pullBranch(branch);
await gitUtils.push(branch, { includeTags: true });
let { packages, tool } = await getPackages(cwd);
let releasedPackages: Package[] = [];
if (tool !== "root") {
let newTagRegex = /New tag:\s+(@[^/]+\/[^@]+|[^/]+)@([^\s]+)/;
let packagesByName = new Map(packages.map((x) => [x.packageJson.name, x]));
for (let line of changesetPublishOutput.stdout.split("\n")) {
let match = line.match(newTagRegex);
if (match === null) {
continue;
}
let pkgName = match[1];
let pkg = packagesByName.get(pkgName);
if (pkg === undefined) {
throw new Error(
`Package "${pkgName}" not found.` +
"This is probably a bug in the action, please open an issue"
);
}
releasedPackages.push(pkg);
}
await Promise.all(
releasedPackages.map((pkg) =>
createRelease(octokit, {
pkg,
tagName: `${pkg.packageJson.name}@${pkg.packageJson.version}`,
})
)
);
} else {
if (packages.length === 0) {
throw new Error(
`No package found.` +
"This is probably a bug in the action, please open an issue"
);
}
const pkg = packages[0];
releasedPackages.push(pkg);
await createRelease(octokit, {
pkg,
tagName: `v${pkg.packageJson.version}`,
});
}
if (releasedPackages.length) {
return {
published: true,
publishedPackages: releasedPackages.map((pkg) => ({
name: pkg.packageJson.name,
version: pkg.packageJson.version,
})),
};
}
return { published: false };
}
type VersionOptions = {
script?: string;
githubToken: string;
cwd?: string;
prTitle?: string;
commitMessage?: string;
hasPublishScript?: boolean;
};
export async function runVersion({
script,
githubToken,
cwd = process.cwd(),
prTitle = "Version Packages",
commitMessage = "Version Packages",
hasPublishScript = false,
}: VersionOptions) {
let repo = `${github.context.repo.owner}/${github.context.repo.repo}`;
let branch = github.context.ref.replace("refs/heads/", "");
let versionBranch = `changeset-release/${branch}`;
let octokit = new github.GitHub(githubToken);
let { preState } = await readChangesetState(cwd);
await gitUtils.switchToMaybeExistingBranch(versionBranch);
await gitUtils.reset(github.context.sha);
let versionsByDirectory = await getVersionsByDirectory(cwd);
if (script) {
let [versionCommand, ...versionArgs] = script.split(/\s+/);
await exec(versionCommand, versionArgs, { cwd });
} else {
// technically
let changesetsCliPkgJson = await require(path.join(
cwd,
"node_modules",
"@changesets",
"cli",
"package.json"
));
let cmd = semver.lt(changesetsCliPkgJson.version, "2.0.0")
? "bump"
: "version";
await exec("node", ["./node_modules/@changesets/cli/bin.js", cmd], { cwd });
}
let searchQuery = `repo:${repo}+state:open+head:${versionBranch}+base:${branch}`;
let searchResultPromise = octokit.search.issuesAndPullRequests({
q: searchQuery,
});
let changedPackages = await getChangedPackages(cwd, versionsByDirectory);
let prBodyPromise = (async () => {
return (
`This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and ${
hasPublishScript
? `the packages will be published to npm automatically`
: `publish to npm yourself or [setup this action to publish automatically](https://github.com/changesets/action#with-publishing)`
}. If you're not ready to do a release yet, that's fine, whenever you add more changesets to ${branch}, this PR will be updated.
${
!!preState
? `
⚠️⚠️⚠️⚠️⚠️⚠️
\`${branch}\` is currently in **pre mode** so this branch has prereleases rather than normal releases. If you want to exit prereleases, run \`changeset pre exit\` on \`${branch}\`.
⚠️⚠️⚠️⚠️⚠️⚠️
`
: ""
}
# Releases
` +
(
await Promise.all(
changedPackages.map(async (pkg) => {
let changelogContents = await fs.readFile(
path.join(pkg.dir, "CHANGELOG.md"),
"utf8"
);
let entry = getChangelogEntry(
changelogContents,
pkg.packageJson.version
);
return {
highestLevel: entry.highestLevel,
private: !!pkg.packageJson.private,
content:
`## ${pkg.packageJson.name}@${pkg.packageJson.version}\n\n` +
entry.content,
};
})
)
)
.filter((x) => x)
.sort(sortTheThings)
.map((x) => x.content)
.join("\n ")
);
})();
const finalPrTitle = `${prTitle}${!!preState ? ` (${preState.tag})` : ""}`;
const finalCommitMessage = `${commitMessage}${
!!preState ? ` (${preState.tag})` : ""
}`;
await gitUtils.commitAll(finalCommitMessage);
await gitUtils.push(versionBranch, { force: true });
let searchResult = await searchResultPromise;
console.log(JSON.stringify(searchResult.data, null, 2));
if (searchResult.data.items.length === 0) {
console.log("creating pull request");
await octokit.pulls.create({
base: branch,
head: versionBranch,
title: finalPrTitle,
body: await prBodyPromise,
...github.context.repo,
});
} else {
octokit.pulls.update({
pull_number: searchResult.data.items[0].number,
title: finalPrTitle,
body: await prBodyPromise,
...github.context.repo,
});
console.log("pull request found");
}
}
+101
View File
@@ -0,0 +1,101 @@
import { getChangelogEntry, BumpLevels, sortTheThings } from "./utils";
let changelog = `# @keystone-alpha/email
## 3.0.1
### Patch Changes
- [19fe6c1b](https://github.com/keystonejs/keystone-5/commit/19fe6c1b):
Move frontmatter in docs into comments
## 3.0.0
### Major Changes
- [2164a779](https://github.com/keystonejs/keystone-5/commit/2164a779):
- Replace jade with pug because Jade was renamed to Pug, and \`jade\` package is outdated
### Patch Changes
- [81dc0be5](https://github.com/keystonejs/keystone-5/commit/81dc0be5):
- Update dependencies
## 2.0.0
- [patch][b69fb9b7](https://github.com/keystonejs/keystone-5/commit/b69fb9b7):
- Update dev devependencies
- [major][f97e4ecf](https://github.com/keystonejs/keystone-5/commit/f97e4ecf):
- Export { emailSender } as the API, rather than a default export
## 1.0.2
- [patch][7417ea3a](https://github.com/keystonejs/keystone-5/commit/7417ea3a):
- Update patch-level dependencies
## 1.0.1
- [patch][1f0bc236](https://github.com/keystonejs/keystone-5/commit/1f0bc236):
- Update the package.json author field to "The Keystone Development Team"
## 1.0.0
- [major] 8b6734ae:
- This is the first release of keystone-alpha (previously voussoir).
All packages in the \`@voussoir\` namespace are now available in the \`@keystone-alpha\` namespace, starting at version \`1.0.0\`.
To upgrade your project you must update any \`@voussoir/<foo>\` dependencies in \`package.json\` to point to \`@keystone-alpha/<foo>: "^1.0.0"\` and update any \`require\`/\`import\` statements in your code.
# @voussoir/email
## 0.0.2
- [patch] 113e16d4:
- Remove unused dependencies
- [patch] 625c1a6d:
- Update mjml-dependency
`;
test("it works", () => {
let entry = getChangelogEntry(changelog, "3.0.0");
expect(entry.content).toMatchSnapshot();
expect(entry.highestLevel).toBe(BumpLevels.major);
});
test("it works", () => {
let entry = getChangelogEntry(changelog, "3.0.1");
expect(entry.content).toMatchSnapshot();
expect(entry.highestLevel).toBe(BumpLevels.patch);
});
test("it sorts the things right", () => {
let things = [
{
name: "a",
highestLevel: BumpLevels.major,
private: true,
},
{
name: "b",
highestLevel: BumpLevels.patch,
private: false,
},
{
name: "c",
highestLevel: BumpLevels.major,
private: false,
},
];
expect(things.sort(sortTheThings)).toMatchSnapshot();
});
+127
View File
@@ -0,0 +1,127 @@
import unified from "unified";
import remarkParse from "remark-parse";
import remarkStringify from "remark-stringify";
// @ts-ignore
import mdastToString from "mdast-util-to-string";
import { exec } from "@actions/exec";
import { getPackages, Package } from "@manypkg/get-packages";
export const BumpLevels = {
dep: 0,
patch: 1,
minor: 2,
major: 3,
} as const;
export async function getVersionsByDirectory(cwd: string) {
let { packages } = await getPackages(cwd);
return new Map(packages.map((x) => [x.dir, x.packageJson.version]));
}
export async function getChangedPackages(
cwd: string,
previousVersions: Map<string, string>
) {
let { packages } = await getPackages(cwd);
let changedPackages = new Set<Package>();
for (let pkg of packages) {
const previousVersion = previousVersions.get(pkg.dir);
if (previousVersion !== pkg.packageJson.version) {
changedPackages.add(pkg);
}
}
return [...changedPackages];
}
export function getChangelogEntry(changelog: string, version: string) {
let ast = unified().use(remarkParse).parse(changelog);
let highestLevel: number = BumpLevels.dep;
let nodes = ast.children as Array<any>;
let headingStartInfo:
| {
index: number;
depth: number;
}
| undefined;
let endIndex: number | undefined;
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
if (node.type === "heading") {
let stringified: string = mdastToString(node);
let match = stringified.toLowerCase().match(/(major|minor|patch)/);
if (match !== null) {
let level = BumpLevels[match[0] as "major" | "minor" | "patch"];
highestLevel = Math.max(level, highestLevel);
}
if (headingStartInfo === undefined && stringified === version) {
headingStartInfo = {
index: i,
depth: node.depth,
};
continue;
}
if (
endIndex === undefined &&
headingStartInfo !== undefined &&
headingStartInfo.depth === node.depth
) {
endIndex = i;
break;
}
}
}
if (headingStartInfo) {
ast.children = (ast.children as any).slice(
headingStartInfo.index + 1,
endIndex
);
}
return {
content: unified().use(remarkStringify).stringify(ast),
highestLevel: highestLevel,
};
}
export async function execWithOutput(
command: string,
args?: string[],
options?: { ignoreReturnCode?: boolean; cwd?: string }
) {
let myOutput = "";
let myError = "";
return {
code: await exec(command, args, {
listeners: {
stdout: (data: Buffer) => {
myOutput += data.toString();
},
stderr: (data: Buffer) => {
myError += data.toString();
},
},
...options,
}),
stdout: myOutput,
stderr: myError,
};
}
export function sortTheThings(
a: { private: boolean; highestLevel: number },
b: { private: boolean; highestLevel: number }
) {
if (a.private === b.private) {
return b.highestLevel - a.highestLevel;
}
if (a.private) {
return 1;
}
return -1;
}