cloudflare/kumo
Publicmirrored from https://github.com/cloudflare/kumoAvailable
ci/scripts/validate-kumo-changeset.ts
232lines · modecode
| 1 | #!/usr/bin/env tsx |
| 2 | |
| 3 | import { readFileSync } from "fs"; |
| 4 | import { join } from "path"; |
| 5 | import { |
| 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 | |
| 17 | const KUMO_PACKAGE_NAME = "@cloudflare/kumo"; |
| 18 | const KUMO_PATH = "packages/kumo"; |
| 19 | const CHANGESET_DIR = ".changeset"; |
| 20 | |
| 21 | interface ChangesetFile { |
| 22 | name: string; |
| 23 | content: string; |
| 24 | packages: string[]; |
| 25 | } |
| 26 | |
| 27 | function 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 | |
| 128 | function 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 | |
| 141 | function 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 | |
| 194 | function 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 | */ |
| 225 | function isLocalContext(): boolean { |
| 226 | return !process.env.CI; |
| 227 | } |
| 228 | |
| 229 | // Run if this is the main module (ES module compatible check) |
| 230 | if (import.meta.url === `file://${process.argv[1]}`) { |
| 231 | main(); |
| 232 | } |
| 233 | |