cloudflare/kumo

Public

mirrored from https://github.com/cloudflare/kumoAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
e80c459b2bfdf3392db8e83bc7d7f37d701062b7

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

ci/scripts/validate-kumo-changeset.ts

232lines · modecode

1#!/usr/bin/env tsx
2
3import { readFileSync } from "fs";
4import { join } from "path";
5import {
6 hasChangesInPath,
7 getNewlyAddedFiles,
8 isPullRequestContext,
9 logPullRequestContext,
10} from "../utils/git-operations";
11
12/**
13 * Validates that a changeset exists for the @cloudflare/kumo package
14 * when files in packages/kumo/ are modified in a pull request.
15 */
16
17const KUMO_PACKAGE_NAME = "@cloudflare/kumo";
18const KUMO_PATH = "packages/kumo";
19const CHANGESET_DIR = ".changeset";
20
21interface ChangesetFile {
22 name: string;
23 content: string;
24 packages: string[];
25}
26
27function main() {
28 console.log(`🔍 Validating changeset for kumo package: ${KUMO_PACKAGE_NAME}`);
29
30 // Check if we're in a validation context (CI PR or local pre-push)
31 const shouldValidate = isPullRequestContext() || isLocalContext();
32
33 // Log detection method for transparency
34 if (isPullRequestContext()) {
35 logPullRequestContext();
36 } else if (isLocalContext()) {
37 console.log("Detected context: Local pre-push hook");
38 }
39
40 if (!shouldValidate) {
41 console.log("Not a validation context, skipping changeset validation");
42 return;
43 }
44
45 console.log("Validating changesets...");
46
47 // Check if kumo files have been modified
48 const hasKumoChanges = checkForKumoChanges();
49 if (!hasKumoChanges) {
50 console.log(
51 "No changes detected in packages/kumo/, skipping changeset validation",
52 );
53 return;
54 }
55
56 console.log("Changes detected in packages/kumo/");
57
58 // Check for newly added changesets in this MR
59 const newChangesets = getNewlyAddedChangesets();
60 const newKumoChangesets = newChangesets.filter((cs) =>
61 cs.packages.includes(KUMO_PACKAGE_NAME),
62 );
63
64 if (newKumoChangesets.length === 0) {
65 // Use CI collapsible section for better visibility (CI only)
66 if (process.env.CI) {
67 console.error(
68 "\x1b[0Ksection_start:" +
69 Date.now() +
70 ":changeset_error\r\x1b[0K\x1b[31;1m❌ CHANGESET VALIDATION FAILED\x1b[0m",
71 );
72 } else {
73 console.error("\x1b[31;1m❌ CHANGESET VALIDATION FAILED\x1b[0m");
74 }
75 console.error("");
76
77 // Check if there are any new changesets at all
78 if (newChangesets.length === 0) {
79 console.error(
80 "\x1b[31;1m❌ ERROR: Changes detected in packages/kumo/ but no NEW changeset files found\x1b[0m",
81 );
82 } else {
83 console.error(
84 "\x1b[31;1m❌ ERROR: Found NEW changeset files, but none target @cloudflare/kumo\x1b[0m",
85 );
86 console.error("");
87 console.error("New changesets found:");
88 newChangesets.forEach((cs) => {
89 console.error(` - ${cs.name} (targets: ${cs.packages.join(", ")})`);
90 });
91 }
92
93 console.error("");
94 console.error("\x1b[33;1m📋 To fix this issue:\x1b[0m");
95 console.error(" 1. Run: \x1b[36mpnpm changeset\x1b[0m");
96 console.error(
97 ' 2. Select "\x1b[36m@cloudflare/kumo\x1b[0m" when prompted',
98 );
99 console.error(
100 " 3. Choose the appropriate change type (patch/minor/major)",
101 );
102 console.error(" 4. Write a clear description of your changes");
103 console.error(" 5. Commit the generated changeset file");
104 console.error("");
105 console.error(
106 "This ensures proper versioning and changelog generation for the kumo package.",
107 );
108 console.error("");
109 if (process.env.CI) {
110 console.error(
111 "\x1b[0Ksection_end:" + Date.now() + ":changeset_error\r\x1b[0K",
112 );
113 }
114
115 process.exit(1);
116 }
117
118 console.log(
119 `✅ Found ${newKumoChangesets.length} NEW changeset(s) for @cloudflare/kumo:`,
120 );
121 newKumoChangesets.forEach((cs) => {
122 console.log(` - ${cs.name}`);
123 });
124
125 console.log("Changeset validation passed!");
126}
127
128function checkForKumoChanges(): boolean {
129 const result = hasChangesInPath(KUMO_PATH);
130
131 if (result === null) {
132 console.warn(
133 "⚠️ Warning: Could not determine if kumo changes exist, assuming they do",
134 );
135 return true;
136 }
137
138 return result;
139}
140
141function getNewlyAddedChangesets(): ChangesetFile[] {
142 // Determine working directory (handle both repo root and packages/kumo contexts)
143 const cwd = process.cwd().includes("packages/kumo") ? "../.." : ".";
144
145 // Get newly added files in .changeset directory
146 const newFiles = getNewlyAddedFiles(CHANGESET_DIR, { cwd });
147
148 // Note: empty array means no new changesets were added in this MR
149 // Do NOT fall back to getChangesets() as that would include existing changesets
150
151 const changesets: ChangesetFile[] = [];
152
153 for (const { status, path: filePath } of newFiles) {
154 // Only consider newly added files (A = Added)
155 if (status !== "A") {
156 continue;
157 }
158
159 const fileName = filePath.split("/").pop();
160
161 // Skip config files and README
162 if (
163 !fileName ||
164 fileName === "config.json" ||
165 fileName === "README.md" ||
166 fileName === "USAGE.md" ||
167 !fileName.endsWith(".md")
168 ) {
169 continue;
170 }
171
172 try {
173 // Resolve file path relative to repo root
174 const repoRoot = process.cwd().includes("packages/kumo") ? "../.." : ".";
175 const fullFilePath = join(repoRoot, filePath);
176 const content = readFileSync(fullFilePath, "utf8");
177 const packages = parseChangesetPackages(content);
178
179 changesets.push({
180 name: fileName,
181 content,
182 packages,
183 });
184 } catch (error) {
185 console.warn(
186 `Warning: Could not parse changeset file ${fileName}: ${error}`,
187 );
188 }
189 }
190
191 return changesets;
192}
193
194function parseChangesetPackages(content: string): string[] {
195 const lines = content.split("\n");
196 const packages: string[] = [];
197
198 // Track whether we're inside the YAML frontmatter section (between --- markers)
199 // where package declarations are defined
200 let inFrontmatter = false;
201 for (const line of lines) {
202 if (line.trim() === "---") {
203 inFrontmatter = !inFrontmatter;
204 continue;
205 }
206
207 if (inFrontmatter) {
208 // Parse YAML-style package declarations
209 // Format: "@package/name": patch|minor|major
210 const match = line.match(
211 /^["']?([^"':]+)["']?\s*:\s*(patch|minor|major)/,
212 );
213 if (match) {
214 packages.push(match[1]);
215 }
216 }
217 }
218
219 return packages;
220}
221
222/**
223 * Checks if we're running in a local development context (not CI)
224 */
225function isLocalContext(): boolean {
226 return !process.env.CI;
227}
228
229// Run if this is the main module (ES module compatible check)
230if (import.meta.url === `file://${process.argv[1]}`) {
231 main();
232}
233