cloudflare/kumo

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

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

Clone

HTTPS

Download ZIP

ci/scripts/validate-kumo-changeset.ts

250lines · 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 // Skip validation on Changesets release PRs. The `changesets/action` bot
46 // opens these PRs from a `changeset-release/<target>` branch and, by
47 // design, their diff modifies `packages/kumo/` (version bump + CHANGELOG)
48 // while removing — not adding — `.changeset/*.md` files. Running the
49 // "must add a new changeset" rule here would always fail. See
50 // https://github.com/changesets/action for the branch-name convention.
51 //
52 // GITHUB_HEAD_REF is the PR's source branch name and is always set on
53 // pull_request workflow runs — the only CI path that reaches this code,
54 // per the isPullRequestContext() guard above.
55 const headRef = process.env.GITHUB_HEAD_REF ?? "";
56 if (headRef.startsWith("changeset-release/")) {
57 console.log(
58 `Detected Changesets release PR (branch: ${headRef}); skipping validation.`,
59 );
60 return;
61 }
62
63 console.log("Validating changesets...");
64
65 // Check if kumo files have been modified
66 const hasKumoChanges = checkForKumoChanges();
67 if (!hasKumoChanges) {
68 console.log(
69 "No changes detected in packages/kumo/, skipping changeset validation",
70 );
71 return;
72 }
73
74 console.log("Changes detected in packages/kumo/");
75
76 // Check for newly added changesets in this MR
77 const newChangesets = getNewlyAddedChangesets();
78 const newKumoChangesets = newChangesets.filter((cs) =>
79 cs.packages.includes(KUMO_PACKAGE_NAME),
80 );
81
82 if (newKumoChangesets.length === 0) {
83 // Use CI collapsible section for better visibility (CI only)
84 if (process.env.CI) {
85 console.error(
86 "\x1b[0Ksection_start:" +
87 Date.now() +
88 ":changeset_error\r\x1b[0K\x1b[31;1m❌ CHANGESET VALIDATION FAILED\x1b[0m",
89 );
90 } else {
91 console.error("\x1b[31;1m❌ CHANGESET VALIDATION FAILED\x1b[0m");
92 }
93 console.error("");
94
95 // Check if there are any new changesets at all
96 if (newChangesets.length === 0) {
97 console.error(
98 "\x1b[31;1m❌ ERROR: Changes detected in packages/kumo/ but no NEW changeset files found\x1b[0m",
99 );
100 } else {
101 console.error(
102 "\x1b[31;1m❌ ERROR: Found NEW changeset files, but none target @cloudflare/kumo\x1b[0m",
103 );
104 console.error("");
105 console.error("New changesets found:");
106 newChangesets.forEach((cs) => {
107 console.error(` - ${cs.name} (targets: ${cs.packages.join(", ")})`);
108 });
109 }
110
111 console.error("");
112 console.error("\x1b[33;1m📋 To fix this issue:\x1b[0m");
113 console.error(" 1. Run: \x1b[36mpnpm changeset\x1b[0m");
114 console.error(
115 ' 2. Select "\x1b[36m@cloudflare/kumo\x1b[0m" when prompted',
116 );
117 console.error(
118 " 3. Choose the appropriate change type (patch/minor/major)",
119 );
120 console.error(" 4. Write a clear description of your changes");
121 console.error(" 5. Commit the generated changeset file");
122 console.error("");
123 console.error(
124 "This ensures proper versioning and changelog generation for the kumo package.",
125 );
126 console.error("");
127 if (process.env.CI) {
128 console.error(
129 "\x1b[0Ksection_end:" + Date.now() + ":changeset_error\r\x1b[0K",
130 );
131 }
132
133 process.exit(1);
134 }
135
136 console.log(
137 `✅ Found ${newKumoChangesets.length} NEW changeset(s) for @cloudflare/kumo:`,
138 );
139 newKumoChangesets.forEach((cs) => {
140 console.log(` - ${cs.name}`);
141 });
142
143 console.log("Changeset validation passed!");
144}
145
146function checkForKumoChanges(): boolean {
147 const result = hasChangesInPath(KUMO_PATH);
148
149 if (result === null) {
150 console.warn(
151 "⚠️ Warning: Could not determine if kumo changes exist, assuming they do",
152 );
153 return true;
154 }
155
156 return result;
157}
158
159function getNewlyAddedChangesets(): ChangesetFile[] {
160 // Determine working directory (handle both repo root and packages/kumo contexts)
161 const cwd = process.cwd().includes("packages/kumo") ? "../.." : ".";
162
163 // Get newly added files in .changeset directory
164 const newFiles = getNewlyAddedFiles(CHANGESET_DIR, { cwd });
165
166 // Note: empty array means no new changesets were added in this MR
167 // Do NOT fall back to getChangesets() as that would include existing changesets
168
169 const changesets: ChangesetFile[] = [];
170
171 for (const { status, path: filePath } of newFiles) {
172 // Only consider newly added files (A = Added)
173 if (status !== "A") {
174 continue;
175 }
176
177 const fileName = filePath.split("/").pop();
178
179 // Skip config files and README
180 if (
181 !fileName ||
182 fileName === "config.json" ||
183 fileName === "README.md" ||
184 fileName === "USAGE.md" ||
185 !fileName.endsWith(".md")
186 ) {
187 continue;
188 }
189
190 try {
191 // Resolve file path relative to repo root
192 const repoRoot = process.cwd().includes("packages/kumo") ? "../.." : ".";
193 const fullFilePath = join(repoRoot, filePath);
194 const content = readFileSync(fullFilePath, "utf8");
195 const packages = parseChangesetPackages(content);
196
197 changesets.push({
198 name: fileName,
199 content,
200 packages,
201 });
202 } catch (error) {
203 console.warn(
204 `Warning: Could not parse changeset file ${fileName}: ${error}`,
205 );
206 }
207 }
208
209 return changesets;
210}
211
212function parseChangesetPackages(content: string): string[] {
213 const lines = content.split("\n");
214 const packages: string[] = [];
215
216 // Track whether we're inside the YAML frontmatter section (between --- markers)
217 // where package declarations are defined
218 let inFrontmatter = false;
219 for (const line of lines) {
220 if (line.trim() === "---") {
221 inFrontmatter = !inFrontmatter;
222 continue;
223 }
224
225 if (inFrontmatter) {
226 // Parse YAML-style package declarations
227 // Format: "@package/name": patch|minor|major
228 const match = line.match(
229 /^["']?([^"':]+)["']?\s*:\s*(patch|minor|major)/,
230 );
231 if (match) {
232 packages.push(match[1]);
233 }
234 }
235 }
236
237 return packages;
238}
239
240/**
241 * Checks if we're running in a local development context (not CI)
242 */
243function isLocalContext(): boolean {
244 return !process.env.CI;
245}
246
247// Run if this is the main module (ES module compatible check)
248if (import.meta.url === `file://${process.argv[1]}`) {
249 main();
250}
251