Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
613 changes: 613 additions & 0 deletions API-DOCUMENTATION-ARCHITECTURE.md

Large diffs are not rendered by default.

392 changes: 392 additions & 0 deletions API-DOCUMENTATION-INVENTORY.md

Large diffs are not rendered by default.

273 changes: 273 additions & 0 deletions API-DOCUMENTATION-PROTOTYPE.md

Large diffs are not rendered by default.

321 changes: 321 additions & 0 deletions API-DOCUMENTATION-TOOLING.md

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,29 @@ EOT
FROM scratch AS release
COPY --from=build /project/public /
COPY --from=pagefind /pagefind /pagefind

# The published musl search binary assumes 4 KB pages. This opt-in rebuild
# supports the 16 KB and 64 KB Linux hosts used for local prototypes as well.
FROM rust:1.94-alpine3.23 AS api-prototype-search
RUN apk add --no-cache build-base
ENV JEMALLOC_SYS_WITH_LG_PAGE=16
RUN cargo install pagefind --version 1.5.2 --locked --root /out

# api-prototype-build is opt-in and never feeds the production release target.
FROM build-base AS api-prototype-build
RUN apk add --no-cache bash diffutils
ENV GOWORK=off
COPY --from=api-prototype-search /out/bin/pagefind /usr/local/bin/prototype-pagefind
ENV PAGEFIND_BIN=/usr/local/bin/prototype-pagefind
ARG API_PROTOTYPE_URL="http://localhost:1314"
ENV API_PROTOTYPE_URL=${API_PROTOTYPE_URL}
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/.npm \
./hack/api-docs/run.sh build

FROM scratch AS api-prototype
COPY --from=api-prototype-build /project/tmp/api-prototype/site /site
COPY --from=api-prototype-build /project/tmp/api-prototype/reports /reports
COPY --from=api-prototype-build /project/tmp/api-prototype/validation.json /validation.json
COPY --from=api-prototype-build /project/tmp/api-prototype/timing.json /timing.json
1 change: 1 addition & 0 deletions _vale/config/vocabularies/Docker/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ nameserver
namespaced?
namespacing
Neovim
Netlify
Npgsql
netfilter
netlabel
Expand Down
8 changes: 7 additions & 1 deletion docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ group "default" {
targets = ["release"]
}

target "api-prototype" {
target = "api-prototype"
output = ["type=local,dest=tmp/api-prototype"]
provenance = false
}

target "index" {
# generate a new local search index
target = "index"
Expand Down Expand Up @@ -188,4 +194,4 @@ target "validate-upstream" {
target = "validate-upstream"
output = ["type=cacheonly"]
provenance = false
}
}
101 changes: 101 additions & 0 deletions hack/api-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# API documentation prototype tooling

This toolchain converts copied API specifications, records upstream changes,
and builds an opt-in Hugo reference. See the
[prototype report](../../API-DOCUMENTATION-PROTOTYPE.md) for findings and limits.

Run from the repository root:

```console
./hack/api-docs/run.sh build
./hack/api-docs/run.sh serve
```

Open `http://localhost:1314/reference/api/`. The local commands need Go 1.26.5,
Hugo 0.163.0, Node.js, npm, and standard shell utilities. The server command also
uses Python 3. The container build supplies its dependencies:

```console
docker buildx bake api-prototype
python3 -m http.server 1314 --bind 127.0.0.1 --directory tmp/api-prototype/site
```

The build exports the complete preview site, including the existing manuals,
under `tmp/api-prototype/site`. It never deploys or submits upstream changes.
The API pages exist only with the prototype Hugo config.

This draft's Netlify deploy previews run the same pipeline and publish
`tmp/api-prototype/site`, using `DEPLOY_PRIME_URL` as the site origin. Open
`/reference/api/` on the deploy preview to explore the catalog. Production
builds retain their existing configuration.

## Commands

| Command | Behavior |
| --- | --- |
| `bootstrap` | Install pinned migration dependencies and build the Go command, Vacuum, and the contract comparison tool |
| `verify` | Verify source digests, replay all migration stages, and compare converted sources, ledgers, and textual diffs |
| `convert` | Recreate converted sources and migration records from immutable snapshots and recorded choices |
| `check` | Run source verification, Vacuum policy rules, and strict validation; recorded exceptions still fail strict validation |
| `generate` | Run the pipeline through prepared data, checked source downloads, and comparison reports, accepting only exact preview exceptions |
| `report` | Check source/model coverage and regenerate the migration summary |
| `test` | Run Go regression tests and migration reproduction |
| `build` | Generate data, build Hugo, preserve reference Markdown, index search, and check output |
| `serve` | Build and serve the preview on localhost |

The low-level Go command accepts `check`, `generate`, or `inspect`, followed by
the repository root. `inspect` writes diagnostics without declaring the input
acceptable. `--preview` accepts only recorded, matching exceptions. Use the
wrapper for the complete publication gates.

## Source and diagnostic records

The [catalog](../../prototypes/api-docs/catalog.json) describes the six snapshots.
The [source lock](../../prototypes/api-docs/source-lock.json) records their
origins and digests. Each migration ledger contains ordered before/after
changes; pointers refer to that stage's input/output document. A converter's
move can appear as a removal and an addition. Textual patches also retain
formatting changes.

Preserve existing operation IDs. Missing IDs come from the checked-in
[ID mapping](../../prototypes/api-docs/operation-ids.json). Add specific,
reviewed correction patches to `corrections.json` beside the catalog when
needed, with the same change classification and evidence fields as the ledger.
Do not edit the original snapshots or authoritative specifications.

Validation exceptions include the API, source digest, rule, pointer, diagnostic
hash, and reason. They are deliberate review records, not an automatically
refreshed allowlist. A changed source or diagnostic invalidates an exception.
`check` fails until the source is ready even when `build` produces a draft.

## Output and coverage

The Go command writes presentation model version 1 under
`tmp/api-prototype/data`. It contains effective operations and the original
schema graph. Hugo templates consume that interface. Source packages contain
both specifications, the migration ledger, textual diff, lock, and exceptions.
The six publication specifications have no external references; external
resource handling is exercised separately by fixtures.

Coverage reports compare operation identities, full operation objects, media
variants, headers, and named schemas. Output checks verify HTML/Markdown pairs
and preview-owned links. The browser checks export a function accepting a
Playwright `Page`; run it against the local preview with JavaScript enabled.

The source loader preserves YAML timestamp spelling and rejects duplicate
keys. JSON Schema evaluation uses the locked OAS dialect resources, with
format/content annotations left as annotations. Request/response annotations
are displayed; complete directional example evaluation and wire-format
validation remain implementation work.

## Search portability

The published `pagefind` 1.5.2 Linux binary fails on the tested 16 KB-page host.
The local build uses the optional Docker recipe to compile the same release
with `JEMALLOC_SYS_WITH_LG_PAGE=16`. Other local hosts use the published binary.
The prototype container includes that rebuild. This affects the opt-in target;
the production search build is unchanged.

Set `PAGEFIND_BIN` to use a prepared compatible binary. Set
`API_PROTOTYPE_URL` when building for a different preview origin. For the
local server, also set `API_PROTOTYPE_PORT` to the matching port.
160 changes: 160 additions & 0 deletions hack/api-docs/browser-checks.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Run with a Playwright Page connected to the local preview; no product API calls.
export default async function verify(page, base = "http://localhost:1314") {
const results = [];
const assert = (condition, message) => {
if (!condition) throw Error(message);
results.push(message);
};
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto(base + "/api-prototype/");
await page.waitForURL("**/reference/api/");
await page.locator(".api-card").first().waitFor();
assert(
(await page.locator(".api-card").count()) === 5,
"Catalog exposes five latest API references",
);
await page.goto(base + "/reference/api/engine/latest/#operation/SystemPing");
await page.waitForURL(
"**/reference/api/engine/version/v1.56/operations/SystemPing/",
);
assert(
page.url().endsWith("/operations/SystemPing/"),
"Latest alias preserves the operation fragment",
);
await page.goto(base + "/reference/api/engine/version/v1.56/");
assert(
(await page.locator(".api-nav").count()) === 1 &&
(await page.locator("nav.navbar-font").count()) === 0,
"API references use local operation navigation",
);
const back = page.getByRole("link", { name: "← API catalog", exact: true });
await back.focus();
await page.keyboard.press("Enter");
await page.waitForURL("**/reference/api/");
assert(
(await page.locator("nav.navbar-font").count()) === 1 &&
(await page.locator(".api-nav").count()) === 0,
"Back link returns to the catalog and Reference navigation",
);
await page.goto(base + "/reference/api/engine/version/v1.56/");
await page.locator("[data-api-filter]").fill("archive");
const visible = page.locator("[data-api-filter-item]:visible");
assert(
(await visible.count()) > 0 && (await visible.count()) < 108,
"Operation filter narrows Engine operations",
);
assert(
(await visible.allTextContents()).every((t) =>
t.toLowerCase().includes("archive"),
),
"Filter results match their label",
);
await page.locator("[data-api-version]").focus();
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
await page.waitForURL("**/reference/api/engine/version/v1.55/");
assert(
page.url().endsWith("/v1.55/"),
"Keyboard version selection changes the reference",
);
await page.goto(
base + "/reference/api/ai-governance/#operation-listPolicies",
);
await page.waitForURL(
"**/reference/api/ai-governance/operations/listPolicies/",
);
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
await page.locator("[data-api-copy]").focus();
await page.keyboard.press("Enter");
const copied = await page.evaluate(() => navigator.clipboard.readText());
assert(
copied.includes("${TOKEN}") &&
!copied.includes("'Authorization: Bearer $TOKEN'"),
"Copy preserves shell-expandable credentials",
);
const manual = await page
.getByRole("link", { name: "Product manual", exact: false })
.getAttribute("href");
await page.goto(manual.startsWith("http") ? manual : base + manual);
assert(
(await page
.getByRole("link", { name: /Explore the AI Governance API 1 prototype/ })
.count()) === 1,
"Product manual links back to the same API reference",
);
await page.goto(
base + "/reference/api/engine/version/v1.56/operations/SystemPing/",
);
const select = page.locator("[data-api-media-select]");
const options = await select
.locator("option")
.evaluateAll((nodes) => nodes.map((n) => n.value).filter(Boolean));
assert(options.length > 1, "Engine exposes multiple media types");
await select.selectOption(options[0]);
assert(
(await page.locator("[data-api-media][hidden]").count()) > 0,
"Media selection filters without removing source variants",
);
await page.goto(
base + "/reference/api/ai-governance/operations/createPolicy/",
);
const exampleSelect = page.locator("[data-api-example-select]").first();
if (await exampleSelect.count()) {
const n = await exampleSelect.locator("option").count();
if (n > 1) {
await exampleSelect.selectOption({ index: 1 });
assert(
(await page.locator("[data-api-example][hidden]").count()) > 0,
"Example selection updates displayed examples",
);
}
}
const search = await page.evaluate(async () => {
const pf = await import("/pagefind/pagefind.js");
const found = await pf.search("GwPriority");
return Promise.all(
found.results.slice(0, 15).map(async (r) => {
const d = await r.data();
return { url: d.url, title: d.meta.title };
}),
);
});
assert(
search.some(
(r) =>
r.url.includes("/reference/api/engine/version/v1.56/") &&
r.title.includes("1.56"),
),
"Search finds schema properties with API version context",
);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(
base + "/reference/api/ai-governance/operations/listPolicies/",
);
assert(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth + 1,
),
"Operation page fits a narrow viewport",
);
assert(
await page.locator("[data-api-copy]").isVisible(),
"Request example remains usable on a narrow screen",
);
await page.setViewportSize({ width: 1440, height: 1000 });
const noJS = await page
.context()
.browser()
.newContext({ javaScriptEnabled: false });
const staticPage = await noJS.newPage();
await staticPage.goto(
base + "/reference/api/ai-governance/operations/listPolicies/",
);
assert(
(await staticPage.locator("[data-api-variant]").count()) > 0 &&
(await staticPage.locator(".api-signature").isVisible()),
"Reference content is present without JavaScript",
);
await noJS.close();
return { result: "pass", checks: results, search };
}
34 changes: 34 additions & 0 deletions hack/api-docs/flatten-preview.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// The site postprocessor rewrites URL-like strings inside fenced JSON too.
// Keep generated reference Markdown intact; its links already target published URLs.
import fs from "node:fs";
import path from "node:path";
import { execFileSync } from "node:child_process";
const root = path.resolve(import.meta.dirname, "../..");
const site = path.join(root, "tmp/api-prototype/site");
const data = JSON.parse(
fs.readFileSync(path.join(root, "tmp/api-prototype/data/api-prototype.json")),
);
const urls = [
"/reference/api/",
...data.apis.flatMap((api) => [
api.url,
...api.operations.map((op) => op.url),
...api.schemas.map((schema) => schema.url),
]),
];
const saved = urls
.map((url) => path.join(site, url, "index.md"))
.map((p) => [p, fs.readFileSync(p)]);
for (const [p] of saved) fs.unlinkSync(p);
try {
execFileSync(
process.execPath,
[path.join(root, "hack/flatten-and-resolve.js"), site],
{ stdio: "inherit" },
);
} finally {
for (const [p, b] of saved) {
const dest = path.basename(p) === "index.md" ? path.dirname(p) + ".md" : p;
fs.writeFileSync(dest, b);
}
}
18 changes: 18 additions & 0 deletions hack/api-docs/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/docker/docs/hack/api-docs

go 1.26.5

require (
github.com/pb33f/libopenapi v0.38.7
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
)

require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/pb33f/jsonpath v0.8.2 // indirect
github.com/pb33f/ordered-map/v2 v2.3.1 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect
)
Loading