1
0
mirror of https://github.com/docker/docs.git synced 2026-10-23 18:09:02 +03:00
Files
docs/hack/validate-whats-new.mjs
David Karlsson 678542f456 docs: add automated what's new highlights (#25729)
## Summary

Replace the static Featured topics section with a data-driven Whatʼs new
timeline. Show the five most important launches by default and let
readers expand the full curated 30-day archive.

Run a daily Claude Sonnet 5 curator over a rolling 30-day window, with
manual dispatch support, deterministic validation, and bot-managed pull
requests. Package the editorial workflow as the reusable
`curate-whats-new` repository skill so Docker Agent and other Agent
Skills-compatible clients can invoke the same process.

Preserve the human-reviewed July 13 through August 11, 2026 result and
flexible selection criteria as a regression example outside the skillʼs
runtime context.

@netlify /

[Homepage
preview](https://deploy-preview-25729--docsdocker.netlify.app/)

Generated by Codex
2026-08-12 17:23:31 +02:00

93 lines
2.9 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
const [path, expectedStart, expectedEnd] = process.argv.slice(2);
if (!expectedEnd) {
throw new Error(`usage: ${process.argv[1]} FILE START END`);
}
const data = JSON.parse(fs.readFileSync(path, "utf8"));
const requiredItemKeys = [
"product",
"title",
"description",
"url",
"published",
"source_prs",
"featured",
];
if (data.period_start !== expectedStart) {
throw new Error(`period_start must be ${expectedStart}`);
}
if (data.period_end !== expectedEnd) {
throw new Error(`period_end must be ${expectedEnd}`);
}
if (!Array.isArray(data.items)) {
throw new Error("items must be an array");
}
for (const [index, item] of data.items.entries()) {
const missing = requiredItemKeys.filter((key) => !(key in item));
if (missing.length > 0) {
throw new Error(`item ${index + 1} is missing: ${missing.join(", ")}`);
}
for (const key of requiredItemKeys.slice(0, 4)) {
if (typeof item[key] !== "string" || item[key].trim() === "") {
throw new Error(`item ${index + 1} has an empty ${key}`);
}
}
if (!item.url.startsWith("/") || item.url.startsWith("/manuals/")) {
throw new Error(`item ${index + 1} url must be an internal published URL`);
}
if (
typeof item.published !== "string" ||
!/^\d{4}-\d{2}-\d{2}$/.test(item.published) ||
new Date(`${item.published}T00:00:00Z`).toISOString().slice(0, 10) !==
item.published
) {
throw new Error(`item ${index + 1} has an invalid published date`);
}
if (item.published < data.period_start || item.published > data.period_end) {
throw new Error(`item ${index + 1} published date is outside the period`);
}
if (
!Array.isArray(item.source_prs) ||
item.source_prs.length === 0 ||
!item.source_prs.every((number) => Number.isInteger(number) && number > 0)
) {
throw new Error(
`item ${index + 1} source_prs must contain pull request numbers`,
);
}
if (typeof item.featured !== "boolean") {
throw new Error(`item ${index + 1} featured must be a boolean`);
}
}
const featuredCount = data.items.filter((item) => item.featured).length;
const expectedFeaturedCount = Math.min(5, data.items.length);
if (featuredCount !== expectedFeaturedCount) {
throw new Error(
`items must contain ${expectedFeaturedCount} featured highlights`,
);
}
const publishedDates = data.items.map((item) => item.published);
const sortedDates = [...publishedDates].sort().reverse();
if (publishedDates.join() !== sortedDates.join()) {
throw new Error("items must be sorted by published date, newest first");
}
const titles = data.items.map((item) => item.title);
if (new Set(titles).size !== titles.length) {
throw new Error("items must not contain duplicate titles");
}
const urls = data.items.map((item) => item.url);
if (new Set(urls).size !== urls.length) {
throw new Error("items must not contain duplicate URLs");
}