mirror of
https://github.com/changesets/action
synced 2026-10-26 16:39:56 +08:00
* Fixed an issue with GitHub releases not being created for successfully published packages when some packages failed to be published * Format --------- Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
441 lines
12 KiB
TypeScript
441 lines
12 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import * as core from "@actions/core";
|
|
import { exec, getExecOutput } from "@actions/exec";
|
|
import * as github from "@actions/github";
|
|
import type { PreState } from "@changesets/types";
|
|
import { type Package, getPackages } from "@manypkg/get-packages";
|
|
import semverLt from "semver/functions/lt.js";
|
|
import { Git } from "./git.ts";
|
|
import type { Octokit } from "./octokit.ts";
|
|
import readChangesetState from "./readChangesetState.ts";
|
|
import {
|
|
getChangedPackages,
|
|
getChangelogEntry,
|
|
getVersionsByDirectory,
|
|
isErrorWithCode,
|
|
sortTheThings,
|
|
} from "./utils.ts";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// GitHub Issues/PRs messages have a max size limit on the
|
|
// message body payload.
|
|
// `body is too long (maximum is 65536 characters)`.
|
|
// To avoid that, we ensure to cap the message to 60k chars.
|
|
const MAX_CHARACTERS_PER_MESSAGE = 60000;
|
|
|
|
const createRelease = async (
|
|
octokit: Octokit,
|
|
{ pkg, tagName }: { pkg: Package; tagName: string },
|
|
) => {
|
|
let changelog;
|
|
try {
|
|
changelog = await fs.readFile(path.join(pkg.dir, "CHANGELOG.md"), "utf8");
|
|
} catch (err) {
|
|
if (isErrorWithCode(err, "ENOENT")) {
|
|
// if we can't find a changelog, the user has probably disabled changelogs
|
|
return;
|
|
}
|
|
throw err;
|
|
}
|
|
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.rest.repos.createRelease({
|
|
name: tagName,
|
|
tag_name: tagName,
|
|
body: changelogEntry.content,
|
|
prerelease: pkg.packageJson.version.includes("-"),
|
|
...github.context.repo,
|
|
});
|
|
};
|
|
|
|
type PublishOptions = {
|
|
script: string;
|
|
githubToken: string;
|
|
octokit: Octokit;
|
|
createGithubReleases: boolean;
|
|
git: Git;
|
|
cwd: string;
|
|
};
|
|
|
|
type PublishedPackage = { name: string; version: string };
|
|
|
|
type PublishResult =
|
|
| {
|
|
published: true;
|
|
publishedPackages: PublishedPackage[];
|
|
exitCode: number;
|
|
}
|
|
| {
|
|
published: false;
|
|
exitCode: number;
|
|
};
|
|
|
|
export async function runPublish({
|
|
script,
|
|
githubToken,
|
|
git,
|
|
octokit,
|
|
createGithubReleases,
|
|
cwd,
|
|
}: PublishOptions): Promise<PublishResult> {
|
|
let changesetPublishOutput = await getExecOutput(script, undefined, {
|
|
cwd,
|
|
ignoreReturnCode: true,
|
|
env: { ...process.env, GITHUB_TOKEN: githubToken },
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
if (createGithubReleases) {
|
|
await Promise.all(
|
|
releasedPackages.map(async (pkg) => {
|
|
const tagName = `${pkg.packageJson.name}@${pkg.packageJson.version}`;
|
|
await git.pushTag(tagName);
|
|
await createRelease(octokit, { pkg, tagName });
|
|
}),
|
|
);
|
|
}
|
|
} else {
|
|
if (packages.length === 0) {
|
|
throw new Error(
|
|
`No package found.` +
|
|
"This is probably a bug in the action, please open an issue",
|
|
);
|
|
}
|
|
let pkg = packages[0];
|
|
let newTagRegex = /New tag:/;
|
|
|
|
for (let line of changesetPublishOutput.stdout.split("\n")) {
|
|
let match = line.match(newTagRegex);
|
|
|
|
if (match) {
|
|
releasedPackages.push(pkg);
|
|
if (createGithubReleases) {
|
|
const tagName = `v${pkg.packageJson.version}`;
|
|
await git.pushTag(tagName);
|
|
await createRelease(octokit, { pkg, tagName });
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (releasedPackages.length) {
|
|
return {
|
|
published: true,
|
|
publishedPackages: releasedPackages.map((pkg) => ({
|
|
name: pkg.packageJson.name,
|
|
version: pkg.packageJson.version,
|
|
})),
|
|
exitCode: changesetPublishOutput.exitCode,
|
|
};
|
|
}
|
|
|
|
return { published: false, exitCode: changesetPublishOutput.exitCode };
|
|
}
|
|
|
|
const requireChangesetsCliPkgJson = (cwd: string) => {
|
|
try {
|
|
return require(
|
|
require.resolve("@changesets/cli/package.json", {
|
|
paths: [cwd],
|
|
}),
|
|
);
|
|
} catch (err) {
|
|
if (isErrorWithCode(err, "MODULE_NOT_FOUND")) {
|
|
throw new Error(
|
|
`Have you forgotten to install \`@changesets/cli\` in "${cwd}"?`,
|
|
{ cause: err },
|
|
);
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
type GetMessageOptions = {
|
|
hasPublishScript: boolean;
|
|
branch: string;
|
|
changedPackagesInfo: {
|
|
highestLevel: number;
|
|
private: boolean;
|
|
content: string;
|
|
header: string;
|
|
}[];
|
|
prBodyMaxCharacters: number;
|
|
preState?: PreState;
|
|
};
|
|
|
|
export async function getVersionPrBody({
|
|
hasPublishScript,
|
|
preState,
|
|
changedPackagesInfo,
|
|
prBodyMaxCharacters,
|
|
branch,
|
|
}: GetMessageOptions) {
|
|
let messageHeader = `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.
|
|
`;
|
|
let messagePrestate = !!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}\`.
|
|
|
|
⚠️⚠️⚠️⚠️⚠️⚠️
|
|
`
|
|
: "";
|
|
let messageReleasesHeading = `# Releases`;
|
|
|
|
let fullMessage = [
|
|
messageHeader,
|
|
messagePrestate,
|
|
messageReleasesHeading,
|
|
...changedPackagesInfo.map((info) => `${info.header}\n\n${info.content}`),
|
|
].join("\n");
|
|
|
|
// Check that the message does not exceed the size limit.
|
|
// If not, omit the changelog entries of each package.
|
|
if (fullMessage.length > prBodyMaxCharacters) {
|
|
fullMessage = [
|
|
messageHeader,
|
|
messagePrestate,
|
|
messageReleasesHeading,
|
|
`\n> The changelog information of each package has been omitted from this message, as the content exceeds the size limit.\n`,
|
|
...changedPackagesInfo.map((info) => `${info.header}\n\n`),
|
|
].join("\n");
|
|
}
|
|
|
|
// Check (again) that the message is within the size limit.
|
|
// If not, omit all release content this time.
|
|
if (fullMessage.length > prBodyMaxCharacters) {
|
|
fullMessage = [
|
|
messageHeader,
|
|
messagePrestate,
|
|
messageReleasesHeading,
|
|
`\n> All release information have been omitted from this message, as the content exceeds the size limit.`,
|
|
].join("\n");
|
|
}
|
|
|
|
return fullMessage;
|
|
}
|
|
|
|
type VersionOptions = {
|
|
script?: string;
|
|
githubToken: string;
|
|
git: Git;
|
|
octokit: Octokit;
|
|
cwd?: string;
|
|
prTitle?: string;
|
|
commitMessage?: string;
|
|
hasPublishScript?: boolean;
|
|
prBodyMaxCharacters?: number;
|
|
prDraft?: "always" | "create";
|
|
branch?: string;
|
|
};
|
|
|
|
type RunVersionResult = {
|
|
pullRequestNumber: number;
|
|
};
|
|
|
|
export async function runVersion({
|
|
script,
|
|
githubToken,
|
|
git,
|
|
octokit,
|
|
cwd = process.cwd(),
|
|
prTitle = "Version Packages",
|
|
commitMessage = "Version Packages",
|
|
hasPublishScript = false,
|
|
prBodyMaxCharacters = MAX_CHARACTERS_PER_MESSAGE,
|
|
branch = github.context.ref.replace("refs/heads/", ""),
|
|
prDraft,
|
|
}: VersionOptions): Promise<RunVersionResult> {
|
|
let versionBranch = `changeset-release/${branch}`;
|
|
|
|
let { preState } = await readChangesetState(cwd);
|
|
|
|
await git.prepareBranch(versionBranch);
|
|
|
|
let versionsByDirectory = await getVersionsByDirectory(cwd);
|
|
|
|
const env = { ...process.env, GITHUB_TOKEN: githubToken };
|
|
|
|
if (script) {
|
|
await exec(script, undefined, { cwd, env });
|
|
} else {
|
|
let changesetsCliPkgJson = requireChangesetsCliPkgJson(cwd);
|
|
let cmd = semverLt(changesetsCliPkgJson.version, "2.0.0")
|
|
? "bump"
|
|
: "version";
|
|
await exec(
|
|
"node",
|
|
[
|
|
require.resolve("@changesets/cli/bin.js", {
|
|
paths: [cwd],
|
|
}),
|
|
cmd,
|
|
],
|
|
{
|
|
cwd,
|
|
env,
|
|
},
|
|
);
|
|
}
|
|
|
|
let changedPackages = await getChangedPackages(cwd, versionsByDirectory);
|
|
let changedPackagesInfoPromises = 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: entry.content,
|
|
header: `## ${pkg.packageJson.name}@${pkg.packageJson.version}`,
|
|
};
|
|
}),
|
|
);
|
|
|
|
const finalPrTitle = `${prTitle}${!!preState ? ` (${preState.tag})` : ""}`;
|
|
const finalCommitMessage = `${commitMessage}${
|
|
!!preState ? ` (${preState.tag})` : ""
|
|
}`;
|
|
|
|
/**
|
|
* Fetch any existing pull requests that are open against the branch,
|
|
* before we push any changes that may inadvertently close the existing PRs.
|
|
*
|
|
* (`@changesets/ghcommit` has to reset the branch to the same commit as the base,
|
|
* which GitHub will then react to by closing the PRs)
|
|
*/
|
|
const existingPullRequests = await octokit.rest.pulls.list({
|
|
...github.context.repo,
|
|
state: "open",
|
|
head: `${github.context.repo.owner}:${versionBranch}`,
|
|
base: branch,
|
|
});
|
|
core.info(
|
|
`Existing pull requests: ${JSON.stringify(
|
|
existingPullRequests.data,
|
|
null,
|
|
2,
|
|
)}`,
|
|
);
|
|
|
|
await git.pushChanges({ branch: versionBranch, message: finalCommitMessage });
|
|
|
|
const changedPackagesInfo = (await changedPackagesInfoPromises)
|
|
.filter((x) => x)
|
|
.sort(sortTheThings);
|
|
|
|
let prBody = await getVersionPrBody({
|
|
hasPublishScript,
|
|
preState,
|
|
branch,
|
|
changedPackagesInfo,
|
|
prBodyMaxCharacters,
|
|
});
|
|
|
|
if (existingPullRequests.data.length === 0) {
|
|
core.info("creating pull request");
|
|
const { data: newPullRequest } = await octokit.rest.pulls.create({
|
|
base: branch,
|
|
head: versionBranch,
|
|
title: finalPrTitle,
|
|
body: prBody,
|
|
draft: prDraft !== undefined,
|
|
...github.context.repo,
|
|
});
|
|
|
|
return {
|
|
pullRequestNumber: newPullRequest.number,
|
|
};
|
|
} else {
|
|
const [pullRequest] = existingPullRequests.data;
|
|
|
|
core.info(`updating found pull request #${pullRequest.number}`);
|
|
const convertPullRequestToDraftMutation =
|
|
prDraft === "always"
|
|
? `
|
|
convertPullRequestToDraft(
|
|
input: {
|
|
pullRequestId: $pullRequestId
|
|
}
|
|
) {
|
|
pullRequest {
|
|
id
|
|
}
|
|
}`
|
|
: "";
|
|
const updatePullRequestMutation = `
|
|
mutation UpdatePullRequest(
|
|
$pullRequestId: ID!
|
|
$title: String!
|
|
$body: String!
|
|
) {
|
|
${convertPullRequestToDraftMutation}
|
|
|
|
updatePullRequest(
|
|
input: {
|
|
pullRequestId: $pullRequestId
|
|
title: $title
|
|
body: $body
|
|
state: OPEN
|
|
}
|
|
) {
|
|
pullRequest {
|
|
id
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
await octokit.graphql(updatePullRequestMutation, {
|
|
pullRequestId: pullRequest.node_id,
|
|
title: finalPrTitle,
|
|
body: prBody,
|
|
});
|
|
|
|
return {
|
|
pullRequestNumber: pullRequest.number,
|
|
};
|
|
}
|
|
}
|