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