Skip to content

Commit 0fd5eb5

Browse files
GuanzhouSongCopilot
andcommitted
Show the v0.116-0 package layout across the site
The package repository now serves the multi-package layout, but the site still described the world before it. /packages hardcoded 0.114-0 (and 0.113-0 for the repository examples), generated an extension-only install command for every target, and stated that "the published package repository does not currently include a gateway package, setup helper, or systemd service" -- which stopped being true for Ubuntu 24.04 and RHEL 9. The Linux Packages Quick Start presented building the gateway from Rust source as the only route to an endpoint. Read the version from the release feed instead of repeating it. The deployment already publishes packages/release-info.json describing the release it mirrors, and nothing consumed it; that is why the page went stale in the first place. The new module derives the DEB, RPM and meta-package versions from real asset filenames, so the page cannot advertise a shape the release does not contain, and falls back to a compiled-in release when the feed is unreachable so the install commands are never blank. Generate the install command per target. Tier-1 targets resolve the per-major stand-alone, which pulls the extension, gateway, tools and documentdb-common; everywhere else keeps the extension command, because offering `documentdb-N` there would be an install command that cannot resolve. PostgreSQL 16 stays on the extension command even on Tier-1, since v0.116-0 narrowed the stack to PostgreSQL 17 and 18 and 16 resolves the older build. Default the selector to Ubuntu 24.04 + PostgreSQL 18. It is the target the release is built and end-to-end tested against; defaulting to Ubuntu 22.04 and PostgreSQL 16 showed first-time visitors the extension-only experience. Document what the packages actually do: the five package roles, the documentdb-setup wizard (including that it prompts for a password, so servers need --admin-password-stdin --yes), connecting with mongosh, verification, the per-major systemd unit names, logs and ports, upgrading, and removal. Also warn that the gateway binds all interfaces by default while the connect example says 127.0.0.1, and how to restrict it. Add a drift check so this cannot silently rot again: when a deployment mirrors a release, the fallback compiled into the bundle must name the same one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8df9084a-ccaf-432c-b015-2ccd8893a9d8
1 parent fcbed7b commit 0fd5eb5

7 files changed

Lines changed: 581 additions & 65 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Fails the build when the site's fallback release drifts from the release the
4+
* package repository actually mirrors.
5+
*
6+
* The /packages page used to hardcode its versions, and they went stale: the
7+
* page still advertised 0.114-0 (and 0.113-0 for the repository examples) after
8+
* v0.116-0 had been published and mirrored. The page now reads
9+
* out/packages/release-info.json at runtime, but it still needs a compiled-in
10+
* fallback for the first paint and for the case where that fetch fails - so the
11+
* fallback can drift in exactly the same way, just less visibly.
12+
*
13+
* This check closes that loop: whenever the deployment mirrors a release, the
14+
* fallback baked into the bundle must name the same one.
15+
*
16+
* Skipped when release-info.json is absent, which is the normal case for a
17+
* site-only build (BUILD_PACKAGES=false, or any fork without the packaging
18+
* secrets). There is nothing to compare against then.
19+
*/
20+
21+
const fs = require("node:fs");
22+
const path = require("node:path");
23+
24+
const releaseInfoPath = path.join(process.cwd(), "out", "packages", "release-info.json");
25+
const fallbackSourcePath = path.join(process.cwd(), "app", "lib", "releaseInfo.ts");
26+
27+
if (!fs.existsSync(releaseInfoPath)) {
28+
console.log(`No ${path.relative(process.cwd(), releaseInfoPath)}; skipping release drift check.`);
29+
process.exit(0);
30+
}
31+
32+
const source = fs.readFileSync(fallbackSourcePath, "utf8");
33+
34+
/** Reads a string field out of the FALLBACK_RELEASE object literal. */
35+
function fallbackField(field) {
36+
const match = new RegExp(`${field}:\\s*"([^"]+)"`).exec(source);
37+
return match ? match[1] : null;
38+
}
39+
40+
const mirrored = JSON.parse(fs.readFileSync(releaseInfoPath, "utf8"));
41+
const mirroredTag = mirrored.tag_name;
42+
if (typeof mirroredTag !== "string" || mirroredTag.length === 0) {
43+
console.error("release-info.json has no tag_name; cannot verify the site's fallback release.");
44+
process.exit(1);
45+
}
46+
47+
const fallbackTag = fallbackField("tagName");
48+
if (fallbackTag !== mirroredTag) {
49+
console.error(
50+
[
51+
"Release drift: the site's fallback release does not match the mirrored release.",
52+
"",
53+
` mirrored (out/packages/release-info.json): ${mirroredTag}`,
54+
` fallback (app/lib/releaseInfo.ts): ${fallbackTag ?? "<not found>"}`,
55+
"",
56+
"Update FALLBACK_RELEASE in app/lib/releaseInfo.ts to the mirrored release.",
57+
"It is what visitors see before the release feed loads, and permanently if",
58+
"that fetch fails.",
59+
].join("\n"),
60+
);
61+
process.exit(1);
62+
}
63+
64+
// The version strings are derived from real asset filenames, so a mismatch here
65+
// means the fallback would render install commands for packages the release
66+
// does not contain.
67+
const assetNames = Array.isArray(mirrored.assets)
68+
? mirrored.assets.map((asset) => asset && asset.name).filter((name) => typeof name === "string")
69+
: [];
70+
71+
const derived = [
72+
{
73+
field: "aptVersion",
74+
pattern: /^ubuntu[\d.]+-postgresql-\d+-documentdb_([^_]+)_/,
75+
},
76+
{
77+
field: "rpmVersion",
78+
pattern: /^rhel\d+-postgresql\d+-documentdb-(.+)\.(?:x86_64|aarch64)\.rpm$/,
79+
},
80+
{
81+
field: "metaVersion",
82+
pattern: /^ubuntu[\d.]+-documentdb_([^_]+)_all\.deb$/,
83+
},
84+
];
85+
86+
let failed = false;
87+
for (const { field, pattern } of derived) {
88+
const actual = assetNames.map((name) => pattern.exec(name)).find((m) => m && m[1]);
89+
if (!actual) {
90+
// The release simply does not ship that package shape; the fallback keeps
91+
// whatever it had, which is not drift.
92+
continue;
93+
}
94+
const expected = actual[1];
95+
const declared = fallbackField(field);
96+
if (declared !== expected) {
97+
console.error(
98+
`Release drift: FALLBACK_RELEASE.${field} is "${declared}" but ${mirroredTag} ships "${expected}".`,
99+
);
100+
failed = true;
101+
}
102+
}
103+
104+
if (failed) {
105+
console.error("\nUpdate FALLBACK_RELEASE in app/lib/releaseInfo.ts.");
106+
process.exit(1);
107+
}
108+
109+
console.log(`Release fallback matches the mirrored release (${mirroredTag}).`);

‎.github/workflows/continuous-deployment.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ jobs:
266266
# downloading ~500 MB of PostgreSQL and PostGIS, which keeps the check to
267267
# a few seconds while still catching the entire "package missing from the
268268
# pool / unsatisfiable dependency" class.
269+
- name: Verify the site's fallback release matches the mirrored release
270+
if: steps.features.outputs.packages == 'true'
271+
run: node .github/scripts/check_release_drift.js
269272
- name: Smoke test the generated repository (dependency resolution)
270273
if: steps.features.outputs.packages == 'true'
271274
run: |

‎app/lib/packageInstall.ts‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,50 @@ const rpmMajorVersions: Record<RpmDistro, "8" | "9"> = {
3939
rhel9: "9",
4040
};
4141

42+
// Distributions where the repository serves the full v0.116-0 package set
43+
// (`documentdb` meta, `documentdb-N`, `documentdb-common`, `documentdb-gateway`,
44+
// `documentdb-postgresql-tools`) rather than the extension package alone.
45+
// v0.116-0 ships Tier-1 only, so everywhere else still resolves the older
46+
// extension-only release and must keep the `postgresql-N-documentdb` command.
47+
export const aptFullStackDistros: readonly AptDistro[] = ["ubuntu24"];
48+
export const rpmFullStackDistros: readonly RpmDistro[] = ["rhel9"];
49+
50+
// The stand-alone packages exist only for the majors the full stack was built
51+
// for. PostgreSQL 16 resolves the older extension-only build even on a
52+
// full-stack distribution, so it must not be offered the stand-alone command.
53+
const fullStackPgVersions = ["17", "18"];
54+
55+
export function aptServesFullStack(
56+
aptTarget: AptDistro,
57+
aptPgVersion: AptPgVersion,
58+
): boolean {
59+
return (
60+
aptFullStackDistros.includes(aptTarget) &&
61+
fullStackPgVersions.includes(aptPgVersion)
62+
);
63+
}
64+
65+
export function rpmServesFullStack(
66+
rpmTarget: RpmDistro,
67+
rpmPgVersion: RpmPgVersion,
68+
): boolean {
69+
return (
70+
rpmFullStackDistros.includes(rpmTarget) &&
71+
fullStackPgVersions.includes(rpmPgVersion)
72+
);
73+
}
74+
4275
export function buildAptInstallCommand(
4376
aptTarget: AptDistro,
4477
aptArch: AptArch,
4578
aptPgVersion: AptPgVersion,
4679
): string {
4780
const pgdgSuite = aptPgdgSuites[aptTarget];
81+
// `documentdb-N` pulls the whole stack (extension + gateway + tools +
82+
// documentdb-common) and owns the systemd lifecycle for that major.
83+
const installTarget = aptServesFullStack(aptTarget, aptPgVersion)
84+
? `documentdb-${aptPgVersion}`
85+
: `postgresql-${aptPgVersion}-documentdb`;
4886

4987
return `sudo apt update && \\
5088
sudo apt install -y curl ca-certificates gnupg && \\
@@ -53,7 +91,7 @@ echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.
5391
curl -fsSL https://documentdb.io/documentdb-archive-keyring.gpg | sudo gpg --dearmor --yes -o /usr/share/keyrings/documentdb-archive-keyring.gpg && \\
5492
echo "deb [arch=${aptArch} signed-by=/usr/share/keyrings/documentdb-archive-keyring.gpg] https://documentdb.io/deb stable ${aptTarget}" | sudo tee /etc/apt/sources.list.d/documentdb.list >/dev/null && \\
5593
sudo apt update && \\
56-
sudo apt install -y postgresql-${aptPgVersion}-documentdb`;
94+
sudo apt install -y ${installTarget}`;
5795
}
5896

5997
export function buildRpmInstallCommand(
@@ -62,6 +100,9 @@ export function buildRpmInstallCommand(
62100
rpmPgVersion: RpmPgVersion,
63101
): string {
64102
const rhelMajorVersion = rpmMajorVersions[rpmTarget];
103+
const installTarget = rpmServesFullStack(rpmTarget, rpmPgVersion)
104+
? `documentdb-${rpmPgVersion}`
105+
: `postgresql${rpmPgVersion}-documentdb`;
65106

66107
return `sudo dnf install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-${rhelMajorVersion}.noarch.rpm && \\
67108
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-${rhelMajorVersion}-${rpmArch}/pgdg-redhat-repo-latest.noarch.rpm && \\
@@ -78,5 +119,12 @@ printf '%s\\n' \\
78119
'enabled=1' \\
79120
'gpgcheck=1' \\
80121
'gpgkey=https://documentdb.io/documentdb-archive-keyring.gpg' | sudo tee /etc/yum.repos.d/documentdb.repo >/dev/null && \\
81-
sudo dnf install -y postgresql${rpmPgVersion}-documentdb`;
122+
sudo dnf install -y ${installTarget}`;
123+
}
124+
125+
// Shown after a full-stack install: the packages ship a wizard that creates the
126+
// PostgreSQL instance, installs the extensions and starts the gateway, so the
127+
// install command alone does not leave a reachable endpoint.
128+
export function buildSetupCommand(): string {
129+
return `sudo documentdb-setup --admin-user admin`;
82130
}

‎app/lib/releaseInfo.ts‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
// The site publishes out/packages/release-info.json on every deployment, built
6+
// from the GitHub release the package repository actually mirrors. It is the
7+
// only authoritative statement of "what version is on documentdb.io", so the
8+
// UI derives its version strings from it rather than repeating them.
9+
//
10+
// Before this module the versions were hardcoded in the page, and they drifted:
11+
// the page still advertised 0.114-0 (and 0.113-0 for the repository examples)
12+
// after v0.116-0 had been published and mirrored.
13+
14+
export type ReleaseInfo = {
15+
/** Git tag of the mirrored release, e.g. "v0.116-0". */
16+
tagName: string;
17+
/** Extension package version on DEB, e.g. "0.116-0". */
18+
aptVersion: string;
19+
/** Extension package version on RPM, e.g. "0.116.0-1.el9". */
20+
rpmVersion: string;
21+
/** Version of every non-extension package, e.g. "0.116.0". */
22+
metaVersion: string;
23+
releaseUrl: string;
24+
assetNames: readonly string[];
25+
};
26+
27+
// Used until the fetch resolves, and permanently if it fails. A stale-but-valid
28+
// page is much better than a blank one, so this is a real release rather than a
29+
// placeholder. Keep it in step with the newest release; the drift check in CI
30+
// fails the build when it falls behind release-info.json.
31+
export const FALLBACK_RELEASE: ReleaseInfo = {
32+
tagName: "v0.116-0",
33+
aptVersion: "0.116-0",
34+
rpmVersion: "0.116.0-1.el9",
35+
metaVersion: "0.116.0",
36+
releaseUrl: "https://github.com/documentdb/documentdb/releases/tag/v0.116-0",
37+
assetNames: [],
38+
};
39+
40+
type RawReleaseInfo = {
41+
tag_name?: unknown;
42+
html_url?: unknown;
43+
assets?: unknown;
44+
};
45+
46+
function assetNamesOf(raw: RawReleaseInfo): string[] {
47+
if (!Array.isArray(raw.assets)) {
48+
return [];
49+
}
50+
return raw.assets
51+
.map((asset) =>
52+
asset && typeof asset === "object" && typeof (asset as { name?: unknown }).name === "string"
53+
? (asset as { name: string }).name
54+
: null,
55+
)
56+
.filter((name): name is string => name !== null);
57+
}
58+
59+
function firstMatch(names: readonly string[], pattern: RegExp): string | null {
60+
for (const name of names) {
61+
const match = pattern.exec(name);
62+
if (match?.[1]) {
63+
return match[1];
64+
}
65+
}
66+
return null;
67+
}
68+
69+
/**
70+
* Derives the display versions from a release-info.json payload.
71+
*
72+
* Each field falls back independently: a release that stops shipping one
73+
* package shape must not blank out the versions that are still present.
74+
*/
75+
export function parseReleaseInfo(payload: unknown): ReleaseInfo {
76+
if (!payload || typeof payload !== "object") {
77+
return FALLBACK_RELEASE;
78+
}
79+
const raw = payload as RawReleaseInfo;
80+
const names = assetNamesOf(raw);
81+
82+
const tagName = typeof raw.tag_name === "string" ? raw.tag_name : FALLBACK_RELEASE.tagName;
83+
const releaseUrl =
84+
typeof raw.html_url === "string"
85+
? raw.html_url
86+
: `https://github.com/documentdb/documentdb/releases/tag/${tagName}`;
87+
88+
// The extension keeps the control-file form (0.116-0) on DEB, while RPM
89+
// splits it into Version/Release and renders 0.116.0-1.el9. Everything else
90+
// uses the flat dotted form. Read all three off real filenames so the page
91+
// cannot claim a shape the release does not contain.
92+
const aptVersion =
93+
firstMatch(names, /^ubuntu[\d.]+-postgresql-\d+-documentdb_([^_]+)_/) ??
94+
firstMatch(names, /^deb\d+-postgresql-\d+-documentdb_([^_]+)_/) ??
95+
FALLBACK_RELEASE.aptVersion;
96+
97+
const rpmVersion =
98+
firstMatch(names, /^rhel\d+-postgresql\d+-documentdb-(.+)\.(?:x86_64|aarch64)\.rpm$/) ??
99+
FALLBACK_RELEASE.rpmVersion;
100+
101+
const metaVersion =
102+
firstMatch(names, /^ubuntu[\d.]+-documentdb_([^_]+)_all\.deb$/) ??
103+
firstMatch(names, /^documentdb-(\d+\.\d+\.\d+)-\d+\.noarch\.rpm$/) ??
104+
FALLBACK_RELEASE.metaVersion;
105+
106+
return { tagName, aptVersion, rpmVersion, metaVersion, releaseUrl, assetNames: names };
107+
}
108+
109+
/**
110+
* Reads the mirrored release description published alongside the packages.
111+
*
112+
* Returns the fallback synchronously so the first paint is always correct-ish,
113+
* then swaps in the live values. The site is a static export, so this has to
114+
* happen in the browser; NEXT_PUBLIC_BASE_PATH is the one base-path value Next
115+
* keeps in the client bundle.
116+
*/
117+
export function useReleaseInfo(): ReleaseInfo {
118+
const [release, setRelease] = useState<ReleaseInfo>(FALLBACK_RELEASE);
119+
120+
useEffect(() => {
121+
let cancelled = false;
122+
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
123+
124+
fetch(`${basePath}/packages/release-info.json`)
125+
.then((response) => (response.ok ? response.json() : Promise.reject(response.status)))
126+
.then((payload) => {
127+
if (!cancelled) {
128+
setRelease(parseReleaseInfo(payload));
129+
}
130+
})
131+
.catch(() => {
132+
// Keep the fallback: an unreachable or malformed feed must not empty
133+
// the install commands the page exists to show.
134+
});
135+
136+
return () => {
137+
cancelled = true;
138+
};
139+
}, []);
140+
141+
return release;
142+
}

0 commit comments

Comments
 (0)