From 678542f456f7f2e6847f310394b57641ba6c04ba Mon Sep 17 00:00:00 2001 From: David Karlsson <35727626+dvdksn@users.noreply.github.com> Date: Wed, 12 Aug 2026 17:23:31 +0200 Subject: [PATCH] docs: add automated what's new highlights (#25729) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- .agents/skills/curate-whats-new/SKILL.md | 74 ++++++++ .../curate-whats-new/agents/openai.yaml | 4 + .../2026-07-13--2026-08-11.json | 96 +++++++++++ .../2026-07-13--2026-08-11.yaml | 39 +++++ .github/agents/whats-new.yaml | 34 ++++ .github/workflows/update-whats-new.yml | 160 ++++++++++++++++++ content/_index.md | 9 +- data/whats-new.json | 96 +++++++++++ hack/validate-whats-new.mjs | 92 ++++++++++ layouts/_partials/whats-new.html | 79 +++++++++ layouts/_shortcodes/whats-new.markdown.md | 9 + layouts/home.html | 38 +---- 12 files changed, 685 insertions(+), 45 deletions(-) create mode 100644 .agents/skills/curate-whats-new/SKILL.md create mode 100644 .agents/skills/curate-whats-new/agents/openai.yaml create mode 100644 .github/agents/whats-new-examples/2026-07-13--2026-08-11.json create mode 100644 .github/agents/whats-new-examples/2026-07-13--2026-08-11.yaml create mode 100644 .github/agents/whats-new.yaml create mode 100644 .github/workflows/update-whats-new.yml create mode 100644 data/whats-new.json create mode 100644 hack/validate-whats-new.mjs create mode 100644 layouts/_partials/whats-new.html create mode 100644 layouts/_shortcodes/whats-new.markdown.md diff --git a/.agents/skills/curate-whats-new/SKILL.md b/.agents/skills/curate-whats-new/SKILL.md new file mode 100644 index 0000000000..c5a25a3188 --- /dev/null +++ b/.agents/skills/curate-whats-new/SKILL.md @@ -0,0 +1,74 @@ +--- +name: curate-whats-new +description: Curate noteworthy Docker launches from documentation pull requests merged during a requested period. Use when generating or reviewing data/whats-new.json, preparing the Docker Docs What's new timeline, or deciding which documented releases merit Docker-wide highlights. +--- + +# Curate What's New + +Treat merged documentation as evidence that a capability shipped, but do not +treat a documentation change as news by itself. + +Include an item only when the merged documentation directly shows all of the +following: + +- A released user-facing feature, material enhancement, or broader availability + milestone that was not available before the period +- A substantial capability or workflow, not new syntax or a small control + within an existing workflow +- Enough Docker-wide editorial significance to merit proactively telling users + about it outside product release notes +- A useful published page and a factual title and description + +Apply a high bar. The result is a curated launch archive, not a complete +changelog. A specialized feature can qualify when its user impact is +substantial. A quiet period can produce few or no items. + +## Exclusions + +Exclude documentation maintenance; fixes; rewrites; guidance for old behavior; +routine release or generated-content syncs; limitations, prerequisites, and +workarounds; narrow flags, settings, command variants, protocols, and +compatibility changes; incremental UI, safety, permissions, or observability +improvements; and lower-level Engine, Build, networking, or storage changes. +These qualify only when they are part of an independently newsworthy +product-level launch. + +Judge the user outcome, not PR size, product popularity, labels, changed lines, +a dedicated page, or the existence of a new API or command. + +## Select highlights + +Include every qualifying launch; do not impose a quota. Mark the five most +important as `featured: true`, or all items when fewer than five qualify. Rank +by the magnitude and distinctness of the user outcome and the value of helping +its audience discover it. Breadth can matter, but a major capability for a +specialized audience can outrank a smaller change for a broad audience. Recency +and product variety are not ranking goals. + +Create one item per launch and combine PRs that document the same launch. +Preserve existing copy while it remains accurate and qualifies. Change featured +status only when the relative importance of the candidate set changes. + +## Procedure + +1. Read `data/whats-new.json`. +2. List every PR merged in the requested period: + + ```console + $ gh pr list --repo docker/docs --state merged --search 'merged:START..END' --limit 200 + ``` + +3. Inspect the diff and resulting pages for every plausible candidate. +4. Decide what qualifies using only evidence in the merged documentation. +5. Replace `period_start`, `period_end`, and `items` in + `data/whats-new.json`. Sort items by `published` date, newest first. +6. Write `.pr-body.md` with the publication period, selected highlights and + source PRs, plus concise reasons for plausible exclusions. + +Each item must contain `product`, `title`, `description`, `url`, `published`, +`source_prs`, and `featured`. Use the canonical product name, a published +internal URL, the merge date in `YYYY-MM-DD` format, and source PR numbers. + +Write factual, restrained copy. Avoid superlatives, promotional language, and +claims about ease or importance. Do not modify tracked files other than +`data/whats-new.json`. diff --git a/.agents/skills/curate-whats-new/agents/openai.yaml b/.agents/skills/curate-whats-new/agents/openai.yaml new file mode 100644 index 0000000000..6b51529d64 --- /dev/null +++ b/.agents/skills/curate-whats-new/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Curate What's New" + short_description: "Curate noteworthy Docker launches from merged docs" + default_prompt: "Use $curate-whats-new to curate Docker launches published during the requested date range." diff --git a/.github/agents/whats-new-examples/2026-07-13--2026-08-11.json b/.github/agents/whats-new-examples/2026-07-13--2026-08-11.json new file mode 100644 index 0000000000..0277ac5b85 --- /dev/null +++ b/.github/agents/whats-new-examples/2026-07-13--2026-08-11.json @@ -0,0 +1,96 @@ +{ + "period_start": "2026-07-13", + "period_end": "2026-08-11", + "items": [ + { + "product": "Docker Desktop", + "title": "Use Docker VMM on Mac and Windows", + "description": "Run Docker Desktop with Docker's container-optimized hypervisor on supported Mac and Windows systems.", + "url": "/desktop/features/vmm/", + "published": "2026-08-10", + "source_prs": [25702, 25726], + "featured": true + }, + { + "product": "Docker Sandboxes", + "title": "Connect sandboxes through an MCP gateway", + "description": "Register local or remote MCP servers on the host, reuse them across sandboxes, and govern server and tool access with Cedar policies.", + "url": "/ai/sandboxes/mcp-gateway/", + "published": "2026-08-06", + "source_prs": [25559, 25707], + "featured": true + }, + { + "product": "Docker Sandboxes", + "title": "Author version 2 sandbox kits", + "description": "Define kit setup, permissions, networking, and API key or OAuth credential requirements with the version 2 kit schema.", + "url": "/ai/sandboxes/customize/kit-reference/#schema-versions", + "published": "2026-08-06", + "source_prs": [25467, 25707], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Run Claude Code with local models", + "description": "Route Claude Code sandbox requests to the bundled local-model server or an existing Ollama installation.", + "url": "/ai/sandboxes/agents/claude-code/#use-a-local-model", + "published": "2026-08-06", + "source_prs": [25641, 25707], + "featured": false + }, + { + "product": "Docker AI Governance", + "title": "Search and forward audit events", + "description": "Search and export policy decisions from Docker Cloud, or forward events to Splunk, Dynatrace, and custom HTTPS endpoints.", + "url": "/ai/sandboxes/governance/audit/", + "published": "2026-08-03", + "source_prs": [25679, 25687], + "featured": true + }, + { + "product": "Docker Desktop", + "title": "Install from the Microsoft Store without administrator privileges", + "description": "Fresh Microsoft Store installations on Windows use per-user mode by default.", + "url": "/desktop/release-notes/#4850", + "published": "2026-08-03", + "source_prs": [25665], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Share agent skills across sandboxes", + "description": "Import skills from supported host agents into a persistent store that sandboxes can share.", + "url": "/ai/sandboxes/workflows/#share-agent-skills", + "published": "2026-07-24", + "source_prs": [25588], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Connect editors and desktop apps over SSH", + "description": "Use a sandbox from VS Code, Cursor, Claude Desktop, ChatGPT, or another SSH-capable tool.", + "url": "/ai/sandboxes/integrations/", + "published": "2026-07-24", + "source_prs": [25557], + "featured": true + }, + { + "product": "Docker Hardened Images", + "title": "Query the DHI catalog from AI assistants", + "description": "Search DHI repositories, inspect images, retrieve SBOMs, check CVEs, and manage mirrors through the DHI MCP server.", + "url": "/dhi/tools/mcp/", + "published": "2026-07-23", + "source_prs": [25589], + "featured": false + }, + { + "product": "Docker Enterprise", + "title": "Authenticate GitHub Actions with OIDC connections", + "description": "Exchange GitHub OIDC tokens for short-lived access to Docker resources without storing long-lived workflow credentials.", + "url": "/enterprise/security/oidc-connections/", + "published": "2026-07-21", + "source_prs": [25336], + "featured": true + } + ] +} diff --git a/.github/agents/whats-new-examples/2026-07-13--2026-08-11.yaml b/.github/agents/whats-new-examples/2026-07-13--2026-08-11.yaml new file mode 100644 index 0000000000..da5a56f648 --- /dev/null +++ b/.github/agents/whats-new-examples/2026-07-13--2026-08-11.yaml @@ -0,0 +1,39 @@ +# Human-reviewed example and regression criteria for the curate-whats-new skill. +# This file is not loaded into the skill's runtime context. +period_start: 2026-07-13 +period_end: 2026-08-11 +example_output: 2026-07-13--2026-08-11.json + +expected_archive: + - Docker VMM on Mac and Windows + - Docker Sandboxes MCP gateway + - Docker Sandboxes kit schema version 2 + - Claude Code with local models + - Docker AI Governance audit search, export, and forwarding + - Microsoft Store per-user installation + - Shared agent skills across sandboxes + - Docker Sandboxes editor and app integrations + - Docker Hardened Images MCP server + - Enterprise OIDC connections + +featured_guidance: + strong: + - Docker VMM on Mac and Windows + - Docker AI Governance audit search, export, and forwarding + - Docker Sandboxes editor and app integrations + - Enterprise OIDC connections + either_is_reasonable: + - Docker Sandboxes MCP gateway + - Docker Sandboxes kit schema version 2 + +must_exclude: + - Gordon safety modes and custom rules + - Swarm direct server return networking + - Engine image mounts + - Unix sockets on Windows + - Narrow flags, command variants, protocol options, and compatibility switches + +notes: + - Do not require product diversity. + - Treat featured choices as editorial judgment, not an exact golden ordering. + - Reject lower-level or incremental changes even when they are technically new. diff --git a/.github/agents/whats-new.yaml b/.github/agents/whats-new.yaml new file mode 100644 index 0000000000..04f9f290a5 --- /dev/null +++ b/.github/agents/whats-new.yaml @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/docker/docker-agent/refs/heads/main/agent-schema.json +models: + claude-sonnet-5: + provider: anthropic + model: claude-sonnet-5 + max_tokens: 4096 + temperature: 0.1 + +agents: + root: + model: claude-sonnet-5 + description: Curates user-facing Docker launches from recently published documentation + instruction: | + Use the curate-whats-new skill for every request. + skills: + - curate-whats-new + + toolsets: + - type: filesystem + tools: + - read_file + - read_multiple_files + - write_file + - list_directory + - directory_tree + - type: shell + +permissions: + allow: + - shell:cmd=gh pr list --* + - shell:cmd=gh pr view --* + - shell:cmd=gh pr diff --* + - shell:cmd=git diff --* + - shell:cmd=git log --* diff --git a/.github/workflows/update-whats-new.yml b/.github/workflows/update-whats-new.yml new file mode 100644 index 0000000000..2d4fec1b79 --- /dev/null +++ b/.github/workflows/update-whats-new.yml @@ -0,0 +1,160 @@ +name: Update What's New + +on: + schedule: + # Review the previous 30 complete UTC days every day at 06:00 UTC. + - cron: "0 6 * * *" + workflow_dispatch: + inputs: + dry-run: + description: "Propose highlights without opening a pull request" + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + id-token: write + +concurrency: + group: update-whats-new + cancel-in-progress: false + +env: + BRANCH_NAME: bot/update-whats-new + +jobs: + update: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repository + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + fetch-depth: 0 + + - name: Set publication period + id: period + run: | + PERIOD_END=$(date -u -d 'yesterday' +%F) + echo "start=$(date -u -d "$PERIOD_END - 29 days" +%F)" >> "$GITHUB_OUTPUT" + echo "end=$PERIOD_END" >> "$GITHUB_OUTPUT" + + - name: Configure AWS credentials + id: aws-credentials + continue-on-error: true + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: arn:aws:iam::710015040892:role/docker-agent-action-20260409141318957000000001 + aws-region: us-east-1 + + - name: Fetch bot token + if: steps.aws-credentials.outcome == 'success' + run: | + PAT=$(aws secretsmanager get-secret-value \ + --secret-id docker-agent-action/github-app \ + --query SecretString \ + --output text | jq -r '.pat') + echo "::add-mask::$PAT" + echo "GITHUB_APP_TOKEN=$PAT" >> "$GITHUB_ENV" + + - name: Restore open proposal + env: + GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} + run: | + EXISTING_PR=$(gh pr list --state open --head "$BRANCH_NAME" --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + git fetch origin "$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME" + git show "origin/$BRANCH_NAME:data/whats-new.json" > data/whats-new.json + fi + + - name: Curate highlights + uses: docker/docker-agent-action@e96a4bb40cac114f64358621e1d08346c8eadc8c # v2.0.1 + env: + GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} + with: + agent: ${{ github.workspace }}/.github/agents/whats-new.yaml + prompt: >- + Use the curate-whats-new skill to review documentation pull requests merged from + ${{ steps.period.outputs.start }} through + ${{ steps.period.outputs.end }}, inclusive. + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} + github-token: ${{ env.GITHUB_APP_TOKEN || github.token }} + timeout: 1200 + + - name: Verify agent changes + env: + PERIOD_START: ${{ steps.period.outputs.start }} + PERIOD_END: ${{ steps.period.outputs.end }} + run: | + CHANGED=$(git status --short | awk '$2 != "data/whats-new.json" && $2 != ".pr-body.md" { print }') + if [ -n "$CHANGED" ]; then + echo "::error::Agent changed files outside its allowed scope" + echo "$CHANGED" + exit 1 + fi + test -f .pr-body.md + npx prettier --check data/whats-new.json + node hack/validate-whats-new.mjs data/whats-new.json "$PERIOD_START" "$PERIOD_END" + + - name: Show proposal + if: inputs.dry-run == true + run: | + git diff -- data/whats-new.json + cat .pr-body.md + + - name: Detect changes + id: changes + if: inputs.dry-run != true + run: | + if cmp -s \ + <(git show HEAD:data/whats-new.json | jq -S '.items') \ + <(jq -S '.items' data/whats-new.json); then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Close obsolete pull request + if: steps.changes.outputs.changed == 'false' + env: + GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} + run: | + EXISTING_PR=$(gh pr list --state open --head "$BRANCH_NAME" --json url --jq '.[0].url // empty') + if [ -n "$EXISTING_PR" ]; then + gh pr close "$EXISTING_PR" \ + --comment "The latest sliding-window review no longer proposes changes to the published highlights." + fi + + - name: Commit changes + if: steps.changes.outputs.changed == 'true' + env: + PERIOD_END: ${{ steps.period.outputs.end }} + run: | + git checkout -B "$BRANCH_NAME" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add data/whats-new.json + git commit -m "docs: update what's new for $PERIOD_END" + + - name: Create or update pull request + if: steps.changes.outputs.changed == 'true' + env: + GH_TOKEN: ${{ env.GITHUB_APP_TOKEN || github.token }} + run: | + gh auth setup-git + EXISTING_PR=$(gh pr list --state open --head "$BRANCH_NAME" --json url --jq '.[0].url // empty') + + if [ -n "$EXISTING_PR" ]; then + git push --force --set-upstream origin "$BRANCH_NAME" + gh pr edit "$EXISTING_PR" \ + --title "docs: update what's new" \ + --body-file .pr-body.md + else + git push --force --set-upstream origin "$BRANCH_NAME" + gh pr create \ + --title "docs: update what's new" \ + --body-file .pr-body.md \ + --base main \ + --head "$BRANCH_NAME" + fi diff --git a/content/_index.md b/content/_index.md index e78e0a1cc0..3c9e13e304 100644 --- a/content/_index.md +++ b/content/_index.md @@ -16,14 +16,7 @@ reference material for everyday development and operations tasks. - [Manuals](/manuals/): Install, configure, and use Docker products. - [Reference](/reference/): Browse CLI, API, and file format documentation. -## Featured topics - -- [Docker Hardened Images](/dhi/) -- [Get started with Docker Sandboxes](/ai/sandboxes/get-started/) -- [Docker Desktop overview](/desktop/) -- [Install Docker Engine](/engine/install/) -- [Dockerfile reference](/reference/dockerfile/) -- [Docker Build overview](/build/) +{{< whats-new >}} ## Common questions diff --git a/data/whats-new.json b/data/whats-new.json new file mode 100644 index 0000000000..0277ac5b85 --- /dev/null +++ b/data/whats-new.json @@ -0,0 +1,96 @@ +{ + "period_start": "2026-07-13", + "period_end": "2026-08-11", + "items": [ + { + "product": "Docker Desktop", + "title": "Use Docker VMM on Mac and Windows", + "description": "Run Docker Desktop with Docker's container-optimized hypervisor on supported Mac and Windows systems.", + "url": "/desktop/features/vmm/", + "published": "2026-08-10", + "source_prs": [25702, 25726], + "featured": true + }, + { + "product": "Docker Sandboxes", + "title": "Connect sandboxes through an MCP gateway", + "description": "Register local or remote MCP servers on the host, reuse them across sandboxes, and govern server and tool access with Cedar policies.", + "url": "/ai/sandboxes/mcp-gateway/", + "published": "2026-08-06", + "source_prs": [25559, 25707], + "featured": true + }, + { + "product": "Docker Sandboxes", + "title": "Author version 2 sandbox kits", + "description": "Define kit setup, permissions, networking, and API key or OAuth credential requirements with the version 2 kit schema.", + "url": "/ai/sandboxes/customize/kit-reference/#schema-versions", + "published": "2026-08-06", + "source_prs": [25467, 25707], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Run Claude Code with local models", + "description": "Route Claude Code sandbox requests to the bundled local-model server or an existing Ollama installation.", + "url": "/ai/sandboxes/agents/claude-code/#use-a-local-model", + "published": "2026-08-06", + "source_prs": [25641, 25707], + "featured": false + }, + { + "product": "Docker AI Governance", + "title": "Search and forward audit events", + "description": "Search and export policy decisions from Docker Cloud, or forward events to Splunk, Dynatrace, and custom HTTPS endpoints.", + "url": "/ai/sandboxes/governance/audit/", + "published": "2026-08-03", + "source_prs": [25679, 25687], + "featured": true + }, + { + "product": "Docker Desktop", + "title": "Install from the Microsoft Store without administrator privileges", + "description": "Fresh Microsoft Store installations on Windows use per-user mode by default.", + "url": "/desktop/release-notes/#4850", + "published": "2026-08-03", + "source_prs": [25665], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Share agent skills across sandboxes", + "description": "Import skills from supported host agents into a persistent store that sandboxes can share.", + "url": "/ai/sandboxes/workflows/#share-agent-skills", + "published": "2026-07-24", + "source_prs": [25588], + "featured": false + }, + { + "product": "Docker Sandboxes", + "title": "Connect editors and desktop apps over SSH", + "description": "Use a sandbox from VS Code, Cursor, Claude Desktop, ChatGPT, or another SSH-capable tool.", + "url": "/ai/sandboxes/integrations/", + "published": "2026-07-24", + "source_prs": [25557], + "featured": true + }, + { + "product": "Docker Hardened Images", + "title": "Query the DHI catalog from AI assistants", + "description": "Search DHI repositories, inspect images, retrieve SBOMs, check CVEs, and manage mirrors through the DHI MCP server.", + "url": "/dhi/tools/mcp/", + "published": "2026-07-23", + "source_prs": [25589], + "featured": false + }, + { + "product": "Docker Enterprise", + "title": "Authenticate GitHub Actions with OIDC connections", + "description": "Exchange GitHub OIDC tokens for short-lived access to Docker resources without storing long-lived workflow credentials.", + "url": "/enterprise/security/oidc-connections/", + "published": "2026-07-21", + "source_prs": [25336], + "featured": true + } + ] +} diff --git a/hack/validate-whats-new.mjs b/hack/validate-whats-new.mjs new file mode 100644 index 0000000000..e4e7e05481 --- /dev/null +++ b/hack/validate-whats-new.mjs @@ -0,0 +1,92 @@ +#!/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"); +} diff --git a/layouts/_partials/whats-new.html b/layouts/_partials/whats-new.html new file mode 100644 index 0000000000..e050bc7170 --- /dev/null +++ b/layouts/_partials/whats-new.html @@ -0,0 +1,79 @@ +{{- $data := index hugo.Data "whats-new" -}} +{{- with $data.items -}} + {{- $items := sort . "published" "desc" -}} + {{- $more := where $items "featured" false -}} +
+
+
+

+ What's new +

+
+
    + {{ range $items }} +
  1. + + + + + {{ .product }} + + + + {{ .title }} + + {{ partialCached "icon" "arrow-right" "arrow-right" }} + + + {{ .description }} + +
  2. + {{ end }} +
+ {{ with $more }} + + {{ end }} +
+
+{{- end -}} diff --git a/layouts/_shortcodes/whats-new.markdown.md b/layouts/_shortcodes/whats-new.markdown.md new file mode 100644 index 0000000000..9689ff97da --- /dev/null +++ b/layouts/_shortcodes/whats-new.markdown.md @@ -0,0 +1,9 @@ +{{- $data := index hugo.Data "whats-new" -}} +{{- with $data.items }} + +## What's new + +{{ range . }} +{{ printf "- [%s: %s](%s): %s (%s)\n" .product .title .url .description (.published | time.Format "Jan 2, 2006") -}} +{{ end }} +{{- end }} diff --git a/layouts/home.html b/layouts/home.html index a5f86f937e..1ae6624091 100644 --- a/layouts/home.html +++ b/layouts/home.html @@ -116,43 +116,7 @@ - -
-
-

- Featured topics -

-
- {{ range slice - (dict "url" "/dhi/" "title" "Docker Hardened Images") - (dict "url" "/ai/sandboxes/get-started/" "title" "Get started with Docker Sandboxes") - (dict "url" "/desktop/" "title" "Docker Desktop overview") - (dict "url" "/engine/install/" "title" "Install Docker Engine") - (dict "url" "/reference/dockerfile/" "title" "Dockerfile reference") - (dict "url" "/build/" "title" "Docker Build overview") - }} - - {{ .title }} - - {{ partialCached "icon" "arrow-right" "arrow-right" }} - - - {{ end }} -
-
-
+ {{ partial "whats-new.html" . }}