Skip to content

Commit cd007a3

Browse files
authored
feat: add changeset validation and release workflow (#5673)
* feat: add changeset validation and release workflow * chore: add changes * chore: fix package-lock * chore: update http-proxy-middleware to v4 * docs: update migration guide with ESM-only and Express v5 changes * chore: update webpack-dev-middleware to v8 and document breaking changes * chore: update imports to use node: prefix and replace regex with replaceAll * chore: update qs to v6.15.2 and ws to v8.21.0 in package-lock.json
1 parent fea8432 commit cd007a3

30 files changed

Lines changed: 2908 additions & 4115 deletions

.changeset/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changesets
2+
3+
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4+
with multi-package repos, or single-package repos to help you version and publish your code. You can
5+
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6+
7+
We have a quick list of common questions to get you started engaging with this project in
8+
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

.changeset/bump-dependencies.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": patch
3+
---
4+
5+
Bump production dependencies, notably `open` to v11 and `p-retry` to v8.

.changeset/bump-express-5.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": major
3+
---
4+
5+
Bump Express to v5. See the [Express 5 migration guide](https://expressjs.com/en/guide/migrating-5.html) for the full list of breaking changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": major
3+
---
4+
5+
Bump the `webpack` peer dependency range from `^5.0.0` to `^5.101.0`.

.changeset/changelog-generator.mjs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { getInfo, getInfoFromPullRequest } from "@changesets/get-github-info";
2+
3+
/** @typedef {import("@changesets/types").ChangelogFunctions} ChangelogFunctions */
4+
5+
/**
6+
* @returns {{ GITHUB_SERVER_URL: string }} value
7+
*/
8+
function readEnv() {
9+
const GITHUB_SERVER_URL =
10+
process.env.GITHUB_SERVER_URL || "https://github.com";
11+
return { GITHUB_SERVER_URL };
12+
}
13+
14+
/** @type {ChangelogFunctions} */
15+
const changelogFunctions = {
16+
getDependencyReleaseLine: async (
17+
changesets,
18+
dependenciesUpdated,
19+
options,
20+
) => {
21+
if (!options.repo) {
22+
throw new Error(
23+
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]',
24+
);
25+
}
26+
if (dependenciesUpdated.length === 0) return "";
27+
28+
const changesetLink = `- Updated dependencies [${(
29+
await Promise.all(
30+
changesets.map(async (cs) => {
31+
if (cs.commit) {
32+
const { links } = await getInfo({
33+
repo: options.repo,
34+
commit: cs.commit,
35+
});
36+
return links.commit;
37+
}
38+
}),
39+
)
40+
)
41+
.filter(Boolean)
42+
.join(", ")}]:`;
43+
44+
const updatedDependenciesList = dependenciesUpdated.map(
45+
(dependency) => ` - ${dependency.name}@${dependency.newVersion}`,
46+
);
47+
48+
return [changesetLink, ...updatedDependenciesList].join("\n");
49+
},
50+
getReleaseLine: async (changeset, type, options) => {
51+
const { GITHUB_SERVER_URL } = readEnv();
52+
if (!options || !options.repo) {
53+
throw new Error(
54+
'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]',
55+
);
56+
}
57+
58+
/** @type {number | undefined} */
59+
let prFromSummary;
60+
/** @type {string | undefined} */
61+
let commitFromSummary;
62+
/** @type {string[]} */
63+
const usersFromSummary = [];
64+
65+
const replacedChangelog = changeset.summary
66+
.replace(/^\s*(?:pr|pull|pull\s+request):\s*#?(\d+)/im, (_, pr) => {
67+
const num = Number(pr);
68+
if (!Number.isNaN(num)) prFromSummary = num;
69+
return "";
70+
})
71+
.replace(/^\s*commit:\s*([^\s]+)/im, (_, commit) => {
72+
commitFromSummary = commit;
73+
return "";
74+
})
75+
.replaceAll(/^\s*(?:author|user):\s*@?([^\s]+)/gim, (_, user) => {
76+
usersFromSummary.push(user);
77+
return "";
78+
})
79+
.trim();
80+
81+
const [firstLine, ...futureLines] = replacedChangelog
82+
.split("\n")
83+
.map((l) => l.trimEnd());
84+
85+
const links = await (async () => {
86+
if (prFromSummary !== undefined) {
87+
let { links } = await getInfoFromPullRequest({
88+
repo: options.repo,
89+
pull: prFromSummary,
90+
});
91+
if (commitFromSummary) {
92+
const shortCommitId = commitFromSummary.slice(0, 7);
93+
links = {
94+
...links,
95+
commit: `[\`${shortCommitId}\`](${GITHUB_SERVER_URL}/${options.repo}/commit/${commitFromSummary})`,
96+
};
97+
}
98+
return links;
99+
}
100+
const commitToFetchFrom = commitFromSummary || changeset.commit;
101+
if (commitToFetchFrom) {
102+
const { links } = await getInfo({
103+
repo: options.repo,
104+
commit: commitToFetchFrom,
105+
});
106+
return links;
107+
}
108+
return {
109+
commit: null,
110+
pull: null,
111+
user: null,
112+
};
113+
})();
114+
115+
const users = usersFromSummary.length
116+
? usersFromSummary
117+
.map(
118+
(userFromSummary) =>
119+
`[@${userFromSummary}](${GITHUB_SERVER_URL}/${userFromSummary})`,
120+
)
121+
.join(", ")
122+
: links.user;
123+
124+
let suffix = "";
125+
if (links.pull || links.commit || users) {
126+
suffix = `(${users ? `by ${users} ` : ""}in ${
127+
links.pull || links.commit
128+
})`;
129+
}
130+
131+
return `\n\n- ${firstLine} ${suffix}\n${futureLines
132+
.map((l) => ` ${l}`)
133+
.join("\n")}`;
134+
},
135+
};
136+
137+
export default changelogFunctions;

.changeset/changeset-validate.mjs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/* eslint-disable no-console */
2+
import fs from "node:fs/promises";
3+
import path from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
import { simpleGit } from "simple-git";
6+
import pkgJson from "../package.json" with { type: "json" };
7+
8+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
9+
const rootPath = path.join(__dirname, "..");
10+
const git = simpleGit(rootPath);
11+
12+
const VALID_BUMPS = new Set(["major", "minor", "patch"]);
13+
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
14+
const ENTRY_RE = /^"([^"]+)"\s*:\s*([a-zA-Z]+)\s*$/;
15+
16+
const toLines = (output) =>
17+
output
18+
.split(/\r?\n/)
19+
.map((line) => line.trim())
20+
.filter(Boolean);
21+
22+
const isChangeset = (filePath) => {
23+
const normalized = filePath.replaceAll("\\", "/");
24+
return (
25+
normalized.startsWith(".changeset/") &&
26+
normalized.endsWith(".md") &&
27+
normalized !== ".changeset/README.md"
28+
);
29+
};
30+
31+
const gitDiff = async (more = []) => {
32+
const args = [
33+
"diff",
34+
"--name-only",
35+
// cspell:ignore ACMR
36+
"--diff-filter=ACMR",
37+
...more,
38+
"--",
39+
".changeset/*.md",
40+
].filter(Boolean);
41+
42+
return toLines(await git.raw(args));
43+
};
44+
45+
const getChangedFiles = async () => {
46+
const files = new Set();
47+
const baseRef = process.env.GITHUB_BASE_REF;
48+
49+
// GitHub Actions base diff
50+
if (baseRef) {
51+
for (const file of await gitDiff([`origin/${baseRef}...HEAD`])) {
52+
if (isChangeset(file)) files.add(file);
53+
}
54+
}
55+
// Local working tree changes
56+
else {
57+
const _files = [
58+
// Unstaged changes
59+
...(await gitDiff()),
60+
// Staged but uncommitted changes
61+
...(await gitDiff(["--cached"])),
62+
// Untracked files
63+
...(await git.status()).not_added,
64+
];
65+
for (const file of _files) {
66+
if (isChangeset(file)) files.add(file);
67+
}
68+
}
69+
return files;
70+
};
71+
72+
const validate = async (filePath) => {
73+
const absoluteFilePath = path.join(rootPath, filePath);
74+
const content = await fs.readFile(absoluteFilePath, "utf8");
75+
const frontmatterMatch = content.match(FRONTMATTER_RE);
76+
const errors = [];
77+
78+
if (!frontmatterMatch) {
79+
errors.push("missing YAML frontmatter block");
80+
return errors;
81+
}
82+
83+
const entries = frontmatterMatch[1]
84+
.split(/\r?\n/)
85+
.map((line) => line.trim())
86+
.filter(Boolean);
87+
88+
if (entries.length === 0) {
89+
errors.push("frontmatter does not contain package bump entries");
90+
return errors;
91+
}
92+
93+
for (const entry of entries) {
94+
const match = entry.match(ENTRY_RE);
95+
if (!match) {
96+
errors.push(`invalid frontmatter entry: ${entry}`);
97+
continue;
98+
}
99+
100+
const [, pkgName, bumpType] = match;
101+
if (pkgName !== pkgJson.name) {
102+
errors.push(
103+
`invalid package name "${pkgName}", expected "${pkgJson.name}"`,
104+
);
105+
}
106+
107+
if (!VALID_BUMPS.has(bumpType)) {
108+
errors.push(
109+
`invalid bump type "${bumpType}", expected one of: major, minor, patch`,
110+
);
111+
}
112+
}
113+
114+
return errors;
115+
};
116+
117+
const changedFiles = await getChangedFiles();
118+
119+
if (changedFiles.size === 0) {
120+
console.log("No changed changeset files found.");
121+
} else {
122+
const failures = [];
123+
for (const filePath of changedFiles) {
124+
const errors = await validate(filePath);
125+
for (const error of errors) {
126+
failures.push(`${filePath}: ${error}`);
127+
}
128+
}
129+
130+
if (failures.length > 0) {
131+
console.error("Changeset validation failed:");
132+
for (const failure of failures) {
133+
console.error(`- ${failure}`);
134+
}
135+
process.exitCode = 1;
136+
}
137+
}

.changeset/compression-http2.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": minor
3+
---
4+
5+
Enable the compression middleware for HTTP/2 connections.

.changeset/config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
3+
"changelog": [
4+
"./changelog-generator.mjs",
5+
{ "repo": "webpack/webpack-dev-server" }
6+
],
7+
"fixed": [],
8+
"linked": [],
9+
"access": "public",
10+
"baseBranch": "main",
11+
"updateInternalDependencies": "patch",
12+
"ignore": []
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": major
3+
---
4+
5+
Drop support for Node.js < 22.15.0.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": patch
3+
---
4+
5+
Treat loopback aliases (`127.0.0.1`, `::1`, `localhost`) as equivalent in `isSameOrigin` so the WebSocket client does not reject valid same-origin connections.

0 commit comments

Comments
 (0)