The code and data behind xeiaso.net
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ci): allow scheduled posts with DO NOT MERGE instruction (#1129)

Add support for PRs with future-dated content when the PR body contains
"DO NOT MERGE until YYYY-MM-DD UTC". The validation will pass and post
a warning comment listing the scheduled files and their dates.

- Add @octokit/rest for GitHub API interaction
- Parse PR body for merge-until instruction
- Allow future dates up to and including the merge-until date
- Post warning comment with list of scheduled files
- Fail if post date exceeds the merge-until date

authored by

Xe Iaso and committed by
GitHub
061445eb 64e157ba

+368 -9
+3
.github/workflows/validate-blog-dates.yml
··· 32 32 - name: Validate blog post dates 33 33 env: 34 34 GITHUB_EVENT_NAME: ${{ github.event_name }} 35 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 + GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} 37 + GITHUB_REPOSITORY: ${{ github.repository }} 35 38 run: node scripts/validate-blog-dates.js
+232
package-lock.json
··· 14 14 "jsdom": "^25.0.0" 15 15 }, 16 16 "devDependencies": { 17 + "@octokit/rest": "^21.0.0", 17 18 "@tailwindcss/forms": "^0.5.7", 18 19 "@tailwindcss/typography": "^0.5.14", 19 20 "gray-matter": "^4.0.3", ··· 188 189 "@jridgewell/sourcemap-codec": "^1.4.14" 189 190 } 190 191 }, 192 + "node_modules/@octokit/auth-token": { 193 + "version": "5.1.2", 194 + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", 195 + "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", 196 + "dev": true, 197 + "license": "MIT", 198 + "engines": { 199 + "node": ">= 18" 200 + } 201 + }, 202 + "node_modules/@octokit/core": { 203 + "version": "6.1.6", 204 + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.6.tgz", 205 + "integrity": "sha512-kIU8SLQkYWGp3pVKiYzA5OSaNF5EE03P/R8zEmmrG6XwOg5oBjXyQVVIauQ0dgau4zYhpZEhJrvIYt6oM+zZZA==", 206 + "dev": true, 207 + "license": "MIT", 208 + "dependencies": { 209 + "@octokit/auth-token": "^5.0.0", 210 + "@octokit/graphql": "^8.2.2", 211 + "@octokit/request": "^9.2.3", 212 + "@octokit/request-error": "^6.1.8", 213 + "@octokit/types": "^14.0.0", 214 + "before-after-hook": "^3.0.2", 215 + "universal-user-agent": "^7.0.0" 216 + }, 217 + "engines": { 218 + "node": ">= 18" 219 + } 220 + }, 221 + "node_modules/@octokit/endpoint": { 222 + "version": "10.1.4", 223 + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", 224 + "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", 225 + "dev": true, 226 + "license": "MIT", 227 + "dependencies": { 228 + "@octokit/types": "^14.0.0", 229 + "universal-user-agent": "^7.0.2" 230 + }, 231 + "engines": { 232 + "node": ">= 18" 233 + } 234 + }, 235 + "node_modules/@octokit/graphql": { 236 + "version": "8.2.2", 237 + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", 238 + "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", 239 + "dev": true, 240 + "license": "MIT", 241 + "dependencies": { 242 + "@octokit/request": "^9.2.3", 243 + "@octokit/types": "^14.0.0", 244 + "universal-user-agent": "^7.0.0" 245 + }, 246 + "engines": { 247 + "node": ">= 18" 248 + } 249 + }, 250 + "node_modules/@octokit/openapi-types": { 251 + "version": "25.1.0", 252 + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", 253 + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", 254 + "dev": true, 255 + "license": "MIT" 256 + }, 257 + "node_modules/@octokit/plugin-paginate-rest": { 258 + "version": "11.6.0", 259 + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", 260 + "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", 261 + "dev": true, 262 + "license": "MIT", 263 + "dependencies": { 264 + "@octokit/types": "^13.10.0" 265 + }, 266 + "engines": { 267 + "node": ">= 18" 268 + }, 269 + "peerDependencies": { 270 + "@octokit/core": ">=6" 271 + } 272 + }, 273 + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { 274 + "version": "24.2.0", 275 + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", 276 + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", 277 + "dev": true, 278 + "license": "MIT" 279 + }, 280 + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { 281 + "version": "13.10.0", 282 + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", 283 + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", 284 + "dev": true, 285 + "license": "MIT", 286 + "dependencies": { 287 + "@octokit/openapi-types": "^24.2.0" 288 + } 289 + }, 290 + "node_modules/@octokit/plugin-request-log": { 291 + "version": "5.3.1", 292 + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", 293 + "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", 294 + "dev": true, 295 + "license": "MIT", 296 + "engines": { 297 + "node": ">= 18" 298 + }, 299 + "peerDependencies": { 300 + "@octokit/core": ">=6" 301 + } 302 + }, 303 + "node_modules/@octokit/plugin-rest-endpoint-methods": { 304 + "version": "13.5.0", 305 + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", 306 + "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", 307 + "dev": true, 308 + "license": "MIT", 309 + "dependencies": { 310 + "@octokit/types": "^13.10.0" 311 + }, 312 + "engines": { 313 + "node": ">= 18" 314 + }, 315 + "peerDependencies": { 316 + "@octokit/core": ">=6" 317 + } 318 + }, 319 + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/openapi-types": { 320 + "version": "24.2.0", 321 + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", 322 + "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", 323 + "dev": true, 324 + "license": "MIT" 325 + }, 326 + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { 327 + "version": "13.10.0", 328 + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", 329 + "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", 330 + "dev": true, 331 + "license": "MIT", 332 + "dependencies": { 333 + "@octokit/openapi-types": "^24.2.0" 334 + } 335 + }, 336 + "node_modules/@octokit/request": { 337 + "version": "9.2.4", 338 + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.4.tgz", 339 + "integrity": "sha512-q8ybdytBmxa6KogWlNa818r0k1wlqzNC+yNkcQDECHvQo8Vmstrg18JwqJHdJdUiHD2sjlwBgSm9kHkOKe2iyA==", 340 + "dev": true, 341 + "license": "MIT", 342 + "dependencies": { 343 + "@octokit/endpoint": "^10.1.4", 344 + "@octokit/request-error": "^6.1.8", 345 + "@octokit/types": "^14.0.0", 346 + "fast-content-type-parse": "^2.0.0", 347 + "universal-user-agent": "^7.0.2" 348 + }, 349 + "engines": { 350 + "node": ">= 18" 351 + } 352 + }, 353 + "node_modules/@octokit/request-error": { 354 + "version": "6.1.8", 355 + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", 356 + "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", 357 + "dev": true, 358 + "license": "MIT", 359 + "dependencies": { 360 + "@octokit/types": "^14.0.0" 361 + }, 362 + "engines": { 363 + "node": ">= 18" 364 + } 365 + }, 366 + "node_modules/@octokit/rest": { 367 + "version": "21.1.1", 368 + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", 369 + "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", 370 + "dev": true, 371 + "license": "MIT", 372 + "dependencies": { 373 + "@octokit/core": "^6.1.4", 374 + "@octokit/plugin-paginate-rest": "^11.4.2", 375 + "@octokit/plugin-request-log": "^5.3.1", 376 + "@octokit/plugin-rest-endpoint-methods": "^13.3.0" 377 + }, 378 + "engines": { 379 + "node": ">= 18" 380 + } 381 + }, 382 + "node_modules/@octokit/types": { 383 + "version": "14.1.0", 384 + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", 385 + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", 386 + "dev": true, 387 + "license": "MIT", 388 + "dependencies": { 389 + "@octokit/openapi-types": "^25.1.0" 390 + } 391 + }, 191 392 "node_modules/@parcel/watcher": { 192 393 "version": "2.5.6", 193 394 "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", ··· 795 996 "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 796 997 "license": "MIT" 797 998 }, 999 + "node_modules/before-after-hook": { 1000 + "version": "3.0.2", 1001 + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", 1002 + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", 1003 + "dev": true, 1004 + "license": "Apache-2.0" 1005 + }, 798 1006 "node_modules/call-bind-apply-helpers": { 799 1007 "version": "1.0.2", 800 1008 "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", ··· 1051 1259 "engines": { 1052 1260 "node": ">=0.10.0" 1053 1261 } 1262 + }, 1263 + "node_modules/fast-content-type-parse": { 1264 + "version": "2.0.1", 1265 + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", 1266 + "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", 1267 + "dev": true, 1268 + "funding": [ 1269 + { 1270 + "type": "github", 1271 + "url": "https://github.com/sponsors/fastify" 1272 + }, 1273 + { 1274 + "type": "opencollective", 1275 + "url": "https://opencollective.com/fastify" 1276 + } 1277 + ], 1278 + "license": "MIT" 1054 1279 }, 1055 1280 "node_modules/form-data": { 1056 1281 "version": "4.0.4", ··· 2000 2225 "engines": { 2001 2226 "node": ">=18" 2002 2227 } 2228 + }, 2229 + "node_modules/universal-user-agent": { 2230 + "version": "7.0.3", 2231 + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", 2232 + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", 2233 + "dev": true, 2234 + "license": "ISC" 2003 2235 }, 2004 2236 "node_modules/util-deprecate": { 2005 2237 "version": "1.0.2",
+1
package.json
··· 32 32 "jsdom": "^25.0.0" 33 33 }, 34 34 "devDependencies": { 35 + "@octokit/rest": "^21.0.0", 35 36 "@tailwindcss/forms": "^0.5.7", 36 37 "@tailwindcss/typography": "^0.5.14", 37 38 "gray-matter": "^4.0.3",
+132 -9
scripts/validate-blog-dates.js
··· 4 4 import path from 'path'; 5 5 import { execSync } from 'child_process'; 6 6 import matter from 'gray-matter'; 7 + import { Octokit } from '@octokit/rest'; 8 + 9 + /** 10 + * Parses the PR body for a "DO NOT MERGE until YYYY-MM-DD UTC" instruction 11 + * @param {string} prBody - The PR body text 12 + * @returns {string|null} - The date string if found, null otherwise 13 + */ 14 + function parseMergeUntilDate(prBody) { 15 + if (!prBody) return null; 16 + 17 + // Match patterns like: 18 + // - "DO NOT MERGE until 2026-02-20 UTC" 19 + // - "DO NOT MERGE UNTIL 2026-02-20 UTC" 20 + // - "do not merge until 2026-02-20 UTC" 21 + const pattern = /do not merge until (\d{4}-\d{2}-\d{2}) UTC/i; 22 + const match = prBody.match(pattern); 23 + 24 + return match ? match[1] : null; 25 + } 26 + 27 + /** 28 + * Posts a comment to the PR warning about the merge date 29 + * @param {Octokit} octokit - Octokit instance 30 + * @param {string} owner - Repository owner 31 + * @param {string} repo - Repository name 32 + * @param {number} prNumber - PR number 33 + * @param {string} mergeUntilDate - The date until which the PR should not be merged 34 + * @param {string} filesInfo - Information about the future-dated files 35 + */ 36 + async function postMergeWarningComment(octokit, owner, repo, prNumber, mergeUntilDate, filesInfo) { 37 + const body = `## ⏳ Scheduled Publication Detected 38 + 39 + This PR contains content with future dates that are scheduled for publication: 40 + 41 + ${filesInfo} 42 + 43 + **Do not merge this PR until ${mergeUntilDate} UTC.** 44 + 45 + The content dates have been validated against the "DO NOT MERGE until" date in the PR description. 46 + 47 + > [!NOTE] 48 + > This is an automated message from the validate-blog-dates workflow.`; 49 + 50 + try { 51 + await octokit.rest.issues.createComment({ 52 + owner, 53 + repo, 54 + issue_number: prNumber, 55 + body 56 + }); 57 + console.log(' ✅ Posted warning comment to PR'); 58 + } catch (error) { 59 + console.log(` ⚠️ Failed to post comment: ${error.message}`); 60 + } 61 + } 62 + 63 + /** 64 + * Fetches the PR body using Octokit 65 + * @param {Octokit} octokit - Octokit instance 66 + * @param {string} owner - Repository owner 67 + * @param {string} repo - Repository name 68 + * @param {number} prNumber - PR number 69 + * @returns {Promise<string|null>} - The PR body or null 70 + */ 71 + async function fetchPRBody(octokit, owner, repo, prNumber) { 72 + try { 73 + const { data } = await octokit.rest.pulls.get({ 74 + owner, 75 + repo, 76 + pull_number: prNumber 77 + }); 78 + return data.body; 79 + } catch (error) { 80 + console.log(`Could not fetch PR body: ${error.message}`); 81 + return null; 82 + } 83 + } 7 84 8 85 /** 9 86 * Validates that content files don't have future dates 10 87 */ 11 - function validateContentDates() { 88 + async function validateContentDates() { 12 89 const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format 13 90 const isPullRequest = process.env.GITHUB_EVENT_NAME === 'pull_request'; 14 91 let changedFiles = []; 15 92 let hasErrors = false; 93 + let mergeUntilDate = null; 94 + let futureDatedFiles = []; 16 95 17 96 console.log(`Current date: ${currentDate}`); 18 97 console.log(''); 98 + 99 + // Initialize Octokit if we have a token 100 + const token = process.env.GITHUB_TOKEN; 101 + let octokit = null; 102 + let owner = null; 103 + let repo = null; 104 + 105 + if (token && process.env.GITHUB_REPOSITORY) { 106 + octokit = new Octokit({ auth: token }); 107 + [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); 108 + } 109 + 110 + // Check for merge-until instruction in PR body 111 + if (isPullRequest && octokit && owner && repo && process.env.GITHUB_PR_NUMBER) { 112 + const prBody = await fetchPRBody(octokit, owner, repo, parseInt(process.env.GITHUB_PR_NUMBER)); 113 + mergeUntilDate = parseMergeUntilDate(prBody); 114 + if (mergeUntilDate) { 115 + console.log(`Found "DO NOT MERGE until" instruction: ${mergeUntilDate} UTC`); 116 + console.log(''); 117 + } 118 + } 19 119 20 120 // Get changed files 21 121 if (isPullRequest) { ··· 80 180 console.log(` Date: ${dateToCheck}`); 81 181 82 182 if (dateToCheck > currentDate) { 83 - console.log(' ❌ ERROR: Future date detected!'); 84 - hasErrors = true; 183 + // Check if this is allowed by a merge-until instruction 184 + if (mergeUntilDate && dateToCheck <= mergeUntilDate) { 185 + console.log(` ⏳ Future date allowed by "DO NOT MERGE until ${mergeUntilDate} UTC" instruction`); 186 + futureDatedFiles.push({ file, date: dateToCheck }); 187 + } else if (mergeUntilDate && dateToCheck > mergeUntilDate) { 188 + console.log(` ❌ ERROR: Post date (${dateToCheck}) is after merge-until date (${mergeUntilDate})!`); 189 + hasErrors = true; 190 + } else { 191 + console.log(' ❌ ERROR: Future date detected!'); 192 + hasErrors = true; 193 + } 85 194 } else { 86 195 console.log(' ✅ OK: Date is not in the future'); 87 196 } ··· 93 202 } 94 203 } 95 204 205 + // Post a comment if there are future-dated files that are allowed 206 + if (futureDatedFiles.length > 0 && octokit && owner && repo && process.env.GITHUB_PR_NUMBER) { 207 + const filesInfo = futureDatedFiles.map(f => `- \`${f.file}\` (date: ${f.date})`).join('\n'); 208 + await postMergeWarningComment(octokit, owner, repo, parseInt(process.env.GITHUB_PR_NUMBER), mergeUntilDate, filesInfo); 209 + } 210 + 96 211 if (hasErrors) { 97 212 console.log(''); 98 213 console.log('❌ Validation failed: Found content with future dates!'); 99 - console.log(`All content dates must be on or before the current date (${currentDate})`); 100 - console.log(''); 101 - console.log('To fix this:'); 102 - console.log('1. Update the "date" field in the frontmatter to today\'s date or earlier'); 103 - console.log('2. Use format: date: YYYY-MM-DD'); 214 + if (mergeUntilDate) { 215 + console.log(`Some future dates exceed the "DO NOT MERGE until" date (${mergeUntilDate} UTC)`); 216 + } else { 217 + console.log(`All content dates must be on or before the current date (${currentDate})`); 218 + console.log(''); 219 + console.log('To fix this:'); 220 + console.log('1. Update the "date" field in the frontmatter to today\'s date or earlier'); 221 + console.log('2. Use format: date: YYYY-MM-DD'); 222 + console.log('3. Or add "DO NOT MERGE until YYYY-MM-DD UTC" to your PR description'); 223 + } 104 224 process.exit(1); 105 225 } else { 106 226 console.log('✅ All content dates are valid!'); 227 + if (futureDatedFiles.length > 0) { 228 + console.log(`⚠️ Remember: Do not merge until ${mergeUntilDate} UTC`); 229 + } 107 230 } 108 231 } 109 232 110 233 // Run the validation 111 - validateContentDates(); 234 + validateContentDates();