Message-ID: From: "davecramer (@davecramer)" To: "pgjdbc/pgjdbc" Date: Mon, 01 Jun 2026 11:57:59 +0000 Subject: [pgjdbc/pgjdbc] PR #4123: Pr 4075 restructure List-Id: X-GitHub-Additions: 7488 X-GitHub-Author-Id: 406518 X-GitHub-Author-Login: davecramer X-GitHub-Base: master X-GitHub-Changed-Files: 120 X-GitHub-Commits: 15 X-GitHub-Deletions: 2073 X-GitHub-Draft: true X-GitHub-Head-Branch: pr-4075-restructure X-GitHub-Head-SHA: c60cae50228fc51bfea4e6fd7e9ff7a0acb3b4ab X-GitHub-Issue: 4123 X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: open X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4123 Content-Type: text/plain; charset=utf-8 ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing](https://github.com/pgjdbc/pgjdbc/blob/master/CONTRIBUTING.md) document? * [ ] Have you checked to ensure there aren't other open [Pull Requests](../../pulls) for the same update/change? ### New Feature Submissions: 1. [ ] Does your submission pass tests? 2. [ ] Does `./gradlew styleCheck` pass ? 3. [ ] Have you added your new test classes to an existing test suite in alphabetical order? ### Changes to Existing Features: * [ ] Does this break existing behaviour? If so please explain. * [ ] Have you added an explanation of what your changes do and why you'd like us to include them? * [ ] Have you written new tests for your core changes, as applicable? * [ ] Have you successfully run tests with your changes locally? diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 42f8c114c7..075f5c978d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,20 +20,72 @@ jobs: steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - + with: + # :docs-tools:generateReleaseHistory enumerates REL42.* tags and + # release/42.*.x branches via JGit to build the compatibility + # matrix. A shallow checkout would surface only `master` and + # produce an empty table on the published site. + fetch-depth: 0 + fetch-tags: true + + # Hugo (extended) goes on PATH; the :docs-tools:buildDocs Gradle task + # invokes it via Exec after the release-history generator has produced + # the data files. Keeping the dedicated Hugo install step (instead of + # e.g. `brew install hugo`) lets us pin a tested release without + # relying on whatever the runner happens to ship. - name: Setup Hugo uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3.2.1 with: hugo-version: 'latest' extended: true - + + # JDK is required for the docs build: :docs-tools:generateReleaseHistory + # walks the REL42.* tags via JGit and emits docs/data/release-history.yaml + # before Hugo runs. Same JDK as the main pipeline. + - name: 'Set up JDK 21' + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + distribution: zulu + java-version: 21 + + # enablement: true lets a fresh fork run the workflow without first + # toggling Settings → Pages by hand; the action calls the Pages REST + # API (requires the `pages: write` permission declared above) and + # creates the site in "build from GitHub Actions" mode if missing. + # The `id: pages` is consumed below to forward the auto-detected + # base URL into Hugo. - name: Setup Pages + id: pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - + with: + enablement: true + + # :docs-tools:buildDocs = :docs-tools:generateReleaseHistory then + # `hugo --gc --minify` in docs/. See docs-tools/build.gradle.kts. + # + # HUGO_BASEURL is read by :docs-tools:buildDocs and forwarded to + # hugo as `--baseURL` (CLI flag — config.toml's baseURL is the + # fallback for forks that don't override it). Order of precedence: + # 1. repo variable WEBSITE_BASE_URL (lets pgjdbc/pgjdbc keep + # pinning the canonical https://jdbc.postgresql.org/ URL + # without touching code); + # 2. otherwise the URL that GitHub considers "home" for this + # repo, taken from actions/configure-pages — handles project + # sites, user/org sites, and custom-domain (CNAME) cases. + # If configure-pages reports `http://` (happens on freshly + # enabled sites whose HTTPS cert isn't ready), turn on + # Settings → Pages → Enforce HTTPS in the repo; we don't want to + # paper over a security setting from CI. - name: Build - run: hugo --minify - working-directory: ./docs - + uses: burrunan/gradle-cache-action@663fbad34e03c8f12b27f4999ac46e3d90f87eca # v3.0.1 + env: + HUGO_BASEURL: ${{ vars.WEBSITE_BASE_URL || steps.pages.outputs.base_url }} + S3_BUILD_CACHE_ACCESS_KEY_ID: ${{ secrets.S3_BUILD_CACHE_ACCESS_KEY_ID }} + S3_BUILD_CACHE_SECRET_KEY: ${{ secrets.S3_BUILD_CACHE_SECRET_KEY }} + with: + job-id: docs-build + arguments: --no-parallel --no-daemon --scan :docs-tools:buildDocs + - name: Upload artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: diff --git a/docs-tools/bin/generate-release-history.sh b/docs-tools/bin/generate-release-history.sh new file mode 100755 index 0000000000..5bf221670c --- /dev/null +++ b/docs-tools/bin/generate-release-history.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Generates docs/data/release-history.yaml from local git refs. +# Faster replacement for the Kotlin/JGit version. +# Handles the release-history-overlay.yaml classifiers. + +cd "$(git rev-parse --show-toplevel)" || exit 1 + +OUTPUT="docs/data/release-history.yaml" +OVERLAY="docs/data/release-history-overlay.yaml" +mkdir -p "$(dirname "$OUTPUT")" + +# Collect all tag versions and dates +TAG_DATA=$(git for-each-ref --sort=-version:refname \ + --format='%(refname:strip=2) %(creatordate:short)' \ + 'refs/tags/REL42.*' \ +| grep -v '\-rc' \ +| sed 's/^REL//') + +# Parse overlay classifiers into a temp file for awk +OVERLAY_PARSED=$(mktemp) +trap 'rm -f "$OVERLAY_PARSED"' EXIT + +if [[ -f "$OVERLAY" ]]; then + awk ' + /^[[:space:]]*-[[:space:]]*id:/ { + sub(/.*id:[[:space:]]*/, ""); id = $0 + } + /^[[:space:]]*branch:/ { + sub(/.*branch:[[:space:]]*/, ""); branch = $0 + } + /^[[:space:]]*last_version:/ { + sub(/.*last_version:[[:space:]]*/, ""); ver = $0 + print branch "\t" ver "\t" id + } + ' "$OVERLAY" > "$OVERLAY_PARSED" +fi + +# Generate the full YAML using awk +awk -v classifiers="$OVERLAY_PARSED" ' +BEGIN { + # Read classifiers + nc = 0 + while ((getline cline < classifiers) > 0) { + nc++ + split(cline, parts, "\t") + cbranch[nc] = parts[1] + cversion[nc] = parts[2] + cid[nc] = parts[3] + } + close(classifiers) + + printf "# Generated by :docs-tools:generateReleaseHistory \342\200\224 DO NOT EDIT.\n" + print "# Source of truth: git refs (release/* branches, REL* tags)" + print "# + docs/data/release-history-overlay.yaml." + print "# Run `./gradlew :docs-tools:generateReleaseHistory` to regenerate." + print "" + print "rows:" + n = 0 +} +{ + version = $1 + date = $2 + tag_date[version] = date + + split(version, v, ".") + line = v[1] "." v[2] ".x" + + if (!(line in last)) { + last[line] = version + lastdate[line] = date + order[++n] = line + } + first[line] = version +} +END { + for (i = 1; i <= n; i++) { + l = order[i] + range = (first[l] == last[l]) ? last[l] : first[l] "-" last[l] + printf "- release_line: \"%s\"\n version_range: \"%s\"\n released: \"%s\"\n", l, range, lastdate[l] + + for (j = 1; j <= nc; j++) { + if (cbranch[j] == l) { + cdate = (cversion[j] in tag_date) ? tag_date[cversion[j]] : "" + printf "- release_line: \"%s\"\n version_range: \"%s.%s\"\n released: \"%s\"\n", l, cversion[j], cid[j], cdate + } + } + } +} +' <<< "$TAG_DATA" > "$OUTPUT" + +echo "Wrote $OUTPUT" diff --git a/docs-tools/build.gradle.kts b/docs-tools/build.gradle.kts new file mode 100644 index 0000000000..33eb34d74a --- /dev/null +++ b/docs-tools/build.gradle.kts @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2026, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("build-logic.java-library") + id("build-logic.test-junit5") + id("org.jetbrains.kotlin.jvm") +} + +// docs-tools is a build-time-only utility module. It is NOT shipped with +// the driver jar; nothing here is on the runtime classpath of pgjdbc. + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-stdlib") + + // snakeyaml parses the hand-maintained release-history-overlay.yaml. + implementation("org.yaml:snakeyaml:2.2") +} + +// pgjdbc targets Java 8 bytecode via --release 8; pin Kotlin to the same +// jvmTarget so the two compilers agree (Gradle aborts on a mismatch). +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + freeCompilerArgs.add("-Xjvm-default=all") + } +} + +// docs-tools runs at build time on the project's buildJdk (Java 17+ in +// practice — JGit 7.x already requires Java 17 at runtime). Its tests +// freely use post-Java-8 APIs (e.g. ByteArrayOutputStream#toString(Charset), +// since Java 10), so they cannot run on a Java 8 test toolchain even though +// the produced bytecode is JVM 1.8. +tasks.test { + onlyIf("docs-tools tests use post-Java-8 APIs (e.g. ByteArrayOutputStream#toString(Charset))") { + buildParameters.testJdkVersion > 8 + } +} + +// ===== generateReleaseHistory ============================================ +// +// Scans the local clone's `release/.x` branches and `REL` tags, +// merges with docs/data/release-history-overlay.yaml, emits +// docs/data/release-history.yaml. Consumed by the `release-history` +// Hugo shortcode on the compatibility page. +// +// For CI to produce a complete table the checkout must include all +// `REL42.*` tags and the `release/42.*.x` branches (full-history clone +// or an explicit `git fetch --tags`). +// We pass the project root (where `.git` lives, as either a directory in +// a regular clone or a pointer file in a git worktree); JGit's findGitDir() +// walks up from there and follows the pointer in the worktree case. +val projectRoot = isolated.rootProject.projectDirectory.asFile +val releaseHistoryOverlay = + isolated.rootProject.projectDirectory.dir("docs/data") + .file("release-history-overlay.yaml") +val releaseHistoryYaml = + isolated.rootProject.projectDirectory.dir("docs/data") + .file("release-history.yaml") + +val generateReleaseHistory by tasks.registering(Exec::class) { + group = "documentation" + description = "Generate docs/data/release-history.yaml from git refs " + + "(release/* branches, REL* tags) + release-history-overlay.yaml." + + commandLine("bash", projectRoot.resolve("docs-tools/bin/generate-release-history.sh").absolutePath) + workingDir = projectRoot + // The git directory's content drives the output, but Gradle cannot + // track .git efficiently — declare the overlay as the only file input + // and mark the task non-cacheable on the git side via outputs.upToDateWhen. + inputs.file(releaseHistoryOverlay).withPropertyName("overlay") + outputs.file(releaseHistoryYaml).withPropertyName("releaseHistoryYaml") + outputs.upToDateWhen { false } + +} + +// ----- Hugo wrappers ------------------------------------------------------- +// +// buildDocs — production build: regenerate release-history.yaml, then +// run a one-shot Hugo build. +// +// serveDocs — local dev server: regenerate release-history.yaml, then +// start the Hugo dev server with hot-reload. +// +// Both require a recent extended Hugo on PATH. The doFirst hook fails +// with a readable error if Hugo is missing, too old, or not the +// extended build. + +val docsDir = isolated.rootProject.projectDirectory.dir("docs").asFile + +// The templates track Hugo's current API rather than its deprecated +// surface. site.Language.Locale arrived in 0.158.0 (the release that +// deprecated the older .LanguageCode) and hugo.Data in 0.156.0, so a +// fixed floor is unavoidable. We pin above both and refuse to fall +// back to deprecated APIs: contributors upgrade Hugo instead. Raise +// this when the templates adopt a newer API; do not lower it to +// accommodate an old local install. +val minHugoVersion = "0.161.0" + +fun Exec.requireHugo() { + doFirst { + val versionLine = try { + val process = ProcessBuilder("hugo", "version") + .redirectErrorStream(true) + .start() + val output = process.inputStream.bufferedReader().use { it.readText() } + process.waitFor() + if (process.exitValue() != 0) { + throw GradleException( + "`hugo version` returned exit code ${process.exitValue()}:\n$output" + ) + } + output.trim() + } catch (e: java.io.IOException) { + throw GradleException( + "hugo binary is not on PATH. Install the extended Hugo " + + "$minHugoVersion or newer: https://gohugo.io/installation/. " + + "Underlying error: ${e.message}" + ) + } + + // `hugo version` prints e.g. + // hugo v0.161.1+extended+withdeploy darwin/arm64 BuildDate=... + val match = Regex("""v(\d+)\.(\d+)\.(\d+)""").find(versionLine) + ?: throw GradleException("Could not parse a Hugo version from: $versionLine") + val found = match.destructured.toList().map(String::toInt) + val required = minHugoVersion.split(".").map(String::toInt) + val tooOld = found.zip(required) + .firstOrNull { (f, r) -> f != r } + ?.let { (f, r) -> f < r } + ?: false + if (tooOld) { + throw GradleException( + "This docs build needs the extended Hugo $minHugoVersion or " + + "newer, but found ${match.value.removePrefix("v")}. The " + + "templates use site.Language.Locale (Hugo 0.158.0+) and " + + "hugo.Data (0.156.0+); we track the current API rather than " + + "Hugo's deprecated surface. Upgrade Hugo: " + + "https://gohugo.io/installation/." + ) + } + + // SCSS under docs/assets/sass/ is transpiled by Hugo Pipes, which + // only the extended build ships. A standard build fails mid-render + // with a less obvious message, so reject it up front. + if (!versionLine.contains("+extended")) { + throw GradleException( + "This docs build needs the *extended* Hugo build (SCSS " + + "support), but found a standard build: $versionLine. " + + "Reinstall the extended edition: " + + "https://gohugo.io/installation/." + ) + } + } +} + +val buildDocs by tasks.registering(Exec::class) { + group = "documentation" + description = "Regenerate release-history.yaml, then run a production " + + "Hugo build into docs/public." + dependsOn(generateReleaseHistory) + workingDir = docsDir + + // -PhugoBaseURL=… (or env HUGO_BASEURL) → `hugo --baseURL `. + // The CLI flag wins over both config.toml and Hugo's env-override + // machinery, which has been unreliable for top-level keys since the + // 0.123 config rewrite. CI uses this to point a fork's deploy at + // .github.io// without editing config.toml; locally it + // can be set with `./gradlew :docs-tools:buildDocs -PhugoBaseURL=…`. + val hugoBaseUrl = providers.gradleProperty("hugoBaseURL") + .orElse(providers.environmentVariable("HUGO_BASEURL")) + argumentProviders.add(CommandLineArgumentProvider { + buildList { + add("--gc") + add("--minify") + if (hugoBaseUrl.isPresent) { + add("--baseURL") + add(hugoBaseUrl.get()) + } + } + }) + commandLine("hugo") + requireHugo() +} + +val serveDocs by tasks.registering(Exec::class) { + group = "documentation" + description = "Start the Hugo dev server with hot-reload " + + "(release-history.yaml regenerated from git refs)." + dependsOn(generateReleaseHistory) + workingDir = docsDir + + // -PdocsPort=NNNN overrides the default 1313. + val port = providers.gradleProperty("docsPort").orElse("1313") + argumentProviders.add(CommandLineArgumentProvider { + listOf( + "server", "--bind", "127.0.0.1", "--port", port.get(), + "--disableFastRender" + ) + }) + commandLine("hugo") + requireHugo() +} + +// Lints source files for site-internal links that bypass Hugo's +// BasePath. Three categories of mistake, all of which produce HTML +// that breaks on a project-page deploy (https://.github.io//) +// but happens to "work" on https://jdbc.postgresql.org/ because the +// BasePath is empty there: +// +// 1. Raw `href="/foo"` / `src="/foo"` in templates, not wrapped in +// `| relURL`. Hugo emits the path verbatim, missing `//`. +// +// 2. `{{ "/foo" | relURL }}` with a leading slash. Hugo's relURL +// deliberately treats `/`-prefixed inputs as "you already know +// the absolute path" and does NOT prepend BasePath — so the +// result is identical to (1). +// +// 3. `url = "/foo"` in TOML / `url: "/foo"` in YAML data. The +// consumer template pipes the value through `| relURL`, but +// because the data still starts with `/`, the relURL is a no-op +// (same trap as (2)) — and the data file is then a quiet failure +// mode that's invisible from the template. +// +// Markdown content (`docs/content/**/*.md`) is intentionally NOT +// linted: root-relative markdown links like `[text](/foo)` are the +// idiomatic source-of-truth shape, and the `_default/_markup/render-link.html` +// hook re-runs them through relURL at render time. We're betting that +// no contributor will hand-write `` as raw HTML inside +// a .md file; if that bet breaks, add a fourth check here. +val lintDocsLinks by tasks.registering { + group = "verification" + description = "Lint docs sources for site-internal links that " + + "bypass Hugo's BasePath (root-relative href/src, leading-slash " + + "relURL arguments, `/`-prefixed URLs in data files). Run as a " + + "dependency of :docs-tools:buildDocs." + dependsOn(buildDocs) + + val rootDirAbs = isolated.rootProject.projectDirectory + val layoutsDir = rootDirAbs.dir("docs/layouts").asFile + val dataDir = rootDirAbs.dir("docs/data").asFile + val menusFile = rootDirAbs.file("docs/config/_default/menus.toml").asFile + val repoRoot = rootDirAbs.asFile + + inputs.dir(layoutsDir).withPropertyName("layouts") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.dir(dataDir).withPropertyName("data") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.file(menusFile).withPropertyName("menus") + .withPathSensitivity(PathSensitivity.RELATIVE) + + val stamp = layout.buildDirectory.file("lint-docs-links.stamp") + outputs.file(stamp) + + doLast { + val problems = mutableListOf() + + // Anchor on the literal `"` that opens the attribute value: if + // the very next char is `/` and the one after isn't `/` (so + // protocol-relative `//host` doesn't match), the value Hugo + // emits will start with `/` verbatim. Crucially this does NOT + // match `href="{{ $repo }}/edit/…"` — the first char inside + // the quotes is `{`, not `/` — so multi-piece templates that + // happen to interpolate a `/`-prefixed tail stay clean. + val rawRootAttr = Regex("""\b(href|src)\s*=\s*"(/[a-zA-Z0-9_\-][^"]*)"""") + // `{{ "/foo" | relURL }}` — leading `/` in the relURL arg + // makes Hugo skip BasePath. + val relURLLeadingSlash = Regex( + """\{\{[^}]*"\s*/[a-zA-Z0-9_\-][^"]*"[^}]*\|\s*[^}]*relURL[^}]*\}\}""" + ) + // Multi-line Hugo comments: `{{- /* … */ -}}`. The single-line + // strip can't cover these; track open/close across lines and + // blank out the spanning region before matching. + val commentOpen = Regex("""\{\{-?\s*/\*""") + val commentClose = Regex("""\*/\s*-?\}\}""") + + // render-link.html itself analyzes `/`-prefixed strings as its + // input, so the linter has to skip it (otherwise it'd flag the + // hook that fixes the very problem we're guarding against). + val renderLinkRelative = "_default/_markup/render-link.html" + + layoutsDir.walkTopDown() + .filter { it.isFile && it.extension == "html" } + .filter { + !it.toRelativeString(layoutsDir).replace(File.separatorChar, '/') + .equals(renderLinkRelative) + } + .forEach { f -> + val rel = f.relativeTo(repoRoot).invariantSeparatorsPath + var inComment = false + f.useLines { lines -> + lines.forEachIndexed { idx, rawLine -> + var line = rawLine + if (inComment) { + val close = commentClose.find(line) + if (close != null) { + line = line.substring(close.range.last + 1) + inComment = false + } else { + return@forEachIndexed + } + } + commentOpen.find(line)?.let { open -> + val tail = line.substring(open.range.first) + val close = commentClose.find(tail) + if (close != null) { + line = line.substring(0, open.range.first) + + tail.substring(close.range.last + 1) + } else { + line = line.substring(0, open.range.first) + inComment = true + } + } + + rawRootAttr.findAll(line).forEach { m -> + val attr = m.groupValues[1] + val path = m.groupValues[2] + problems += "$rel:${idx + 1}: ${m.value} — " + + "root-relative `$attr` bypasses Hugo's BasePath. " + + "Wrap in `{{ \"${path.trimStart('/')}\" | relURL }}`." + } + relURLLeadingSlash.findAll(line).forEach { m -> + problems += "$rel:${idx + 1}: ${m.value} — " + + "leading `/` in relURL arg; strip it so Hugo " + + "prepends BasePath." + } + } + } + } + + // Data files: bare `/foo` in `url = …` (TOML) / `url: …` (YAML). + val tomlRootURL = Regex("""^\s*url\s*=\s*"(/[a-zA-Z0-9_\-][^"]*)"""") + val yamlRootURL = Regex("""^\s*url\s*:\s*"?(/[a-zA-Z0-9_\-][^"\s]*)""") + val dataFiles = sequence { + yield(menusFile) + dataDir.walkTopDown().filter { it.isFile }.forEach { yield(it) } + } + dataFiles.filter { it.exists() }.forEach { f -> + val rel = f.relativeTo(repoRoot).invariantSeparatorsPath + val re = when (f.extension.lowercase()) { + "toml" -> tomlRootURL + "yaml", "yml" -> yamlRootURL + else -> return@forEach + } + f.useLines { lines -> + lines.forEachIndexed { idx, line -> + re.find(line)?.let { m -> + problems += "$rel:${idx + 1}: ${m.value.trim()} — " + + "strip leading `/` so the consumer's `| relURL` " + + "prepends BasePath." + } + } + } + } + + val stampFile = stamp.get().asFile + if (problems.isNotEmpty()) { + stampFile.delete() + throw GradleException(buildString { + appendLine( + "Found ${problems.size} root-relative link(s) that " + + "bypass Hugo's BasePath:" + ) + problems.forEach { appendLine(" $it") } + appendLine() + appendLine( + "Site-internal links must go through `relURL` so they " + + "include the repo prefix on project-page deploys " + + "(e.g. https://.github.io//). Markdown " + + "content is handled by the render-link hook; " + + "templates and data files need the fix at the source." + ) + }) + } + stampFile.parentFile.mkdirs() + stampFile.writeText("OK\n") + } +} + +tasks.check { + dependsOn(lintDocsLinks) +} diff --git a/docs-tools/src/main/kotlin/org/postgresql/tools/docs/OverlayLoader.kt b/docs-tools/src/main/kotlin/org/postgresql/tools/docs/OverlayLoader.kt new file mode 100644 index 0000000000..5ff6422816 --- /dev/null +++ b/docs-tools/src/main/kotlin/org/postgresql/tools/docs/OverlayLoader.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.tools.docs + +import org.yaml.snakeyaml.Yaml +import java.io.IOException +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path + +internal data class Classifier( + val branch: String, + val lastVersion: String, + val id: String, +) + +internal data class Overlay( + val classifiers: List = emptyList(), +) + +internal object OverlayLoader { + @Suppress("UNCHECKED_CAST") + fun load(file: Path): Overlay { + if (!Files.isRegularFile(file)) { + System.err.println("Overlay not found at $file — proceeding with no overlay data.") + return Overlay() + } + val root: Map = Files.newBufferedReader(file, StandardCharsets.UTF_8).use { r -> + (Yaml().load(r) as? Map) + ?: throw IOException("Overlay root must be a mapping in $file") + } + return Overlay( + classifiers = (root["classifiers"] as? List>).orEmpty().map { + Classifier( + branch = it["branch"].toString(), + lastVersion = it["last_version"].toString(), + id = it["id"].toString(), + ) + }, + ) + } +} diff --git a/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt new file mode 100644 index 0000000000..06f3849d7b --- /dev/null +++ b/docs-tools/src/test/kotlin/org/postgresql/tools/docs/OverlayLoaderTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2026, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.tools.docs + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import kotlin.io.path.writeText + +class OverlayLoaderTest { + + @TempDir lateinit var tmp: Path + + @Test fun `missing overlay yields the default empty model`() { + val overlay = OverlayLoader.load(tmp.resolve("nope.yaml")) + assertEquals(emptyList(), overlay.classifiers) + } + + @Test fun `classifiers block parses into the Overlay model`() { + val yaml = tmp.resolve("overlay.yaml") + yaml.writeText( + """ + classifiers: + - {id: jre7, branch: 42.2.x, last_version: 42.2.29} + - {id: jre6, branch: 42.2.x, last_version: 42.2.27} + """.trimIndent() + ) + + val overlay = OverlayLoader.load(yaml) + assertEquals( + listOf( + Classifier(branch = "42.2.x", lastVersion = "42.2.29", id = "jre7"), + Classifier(branch = "42.2.x", lastVersion = "42.2.27", id = "jre6"), + ), + overlay.classifiers, + ) + } + + @Test fun `omitted classifiers section falls back to an empty list`() { + val yaml = tmp.resolve("overlay.yaml") + // An overlay containing only an unrelated top-level key should not + // throw — unknown keys are silently ignored, and the model stays + // at defaults. + yaml.writeText("future_field: ignored\n") + + val overlay = OverlayLoader.load(yaml) + assertEquals(emptyList(), overlay.classifiers) + } +} diff --git a/docs/assets/sass/abstracts/_colors.scss b/docs/assets/sass/abstracts/_colors.scss index 9a12cef5cf..1f2f982c9d 100644 --- a/docs/assets/sass/abstracts/_colors.scss +++ b/docs/assets/sass/abstracts/_colors.scss @@ -27,6 +27,40 @@ $colors: ( 200: hsl(0, 0%, 93%), 300: hsl(0, 0%, 88%), 400: hsl(0, 0%, 74%), - 500: hsl(0, 0%, 52%) + 500: hsl(0, 0%, 52%), + 600: hsl(0, 0%, 32%), + 700: hsl(0, 0%, 18%) + ), + + info: ( + 100: hsl(210, 90%, 96%), + 200: hsl(210, 85%, 88%), + 300: hsl(210, 75%, 50%), + 400: hsl(210, 80%, 38%), + 500: hsl(210, 85%, 22%) + ), + + success: ( + 100: hsl(145, 60%, 96%), + 200: hsl(145, 55%, 85%), + 300: hsl(145, 60%, 38%), + 400: hsl(145, 70%, 28%), + 500: hsl(145, 75%, 18%) + ), + + warning: ( + 100: hsl(43, 100%, 95%), + 200: hsl(43, 95%, 80%), + 300: hsl(38, 92%, 50%), + 400: hsl(32, 90%, 40%), + 500: hsl(28, 90%, 25%) + ), + + danger: ( + 100: hsl(0, 80%, 97%), + 200: hsl(0, 70%, 88%), + 300: hsl(0, 70%, 50%), + 400: hsl(0, 75%, 40%), + 500: hsl(0, 80%, 25%) ), ); \ No newline at end of file diff --git a/docs/assets/sass/abstracts/_constants.scss b/docs/assets/sass/abstracts/_constants.scss index 2678821d1f..d124442c66 100644 --- a/docs/assets/sass/abstracts/_constants.scss +++ b/docs/assets/sass/abstracts/_constants.scss @@ -1,10 +1,30 @@ :root { --container-width: 62.5rem; + --prose-width: 46rem; --section-spacing: 3.5em; --container-spacing: 2.5em; --box-spacing: 2em; --primary-spacing: 1em; + + --space-3xs: 0.25rem; + --space-2xs: 0.5rem; + --space-xs: 0.75rem; + --space-s: 1rem; + --space-m: 1.5rem; + --space-l: 2rem; + --space-xl: 3rem; + + --radius-s: 0.25rem; + --radius-m: 0.5rem; + + --border-subtle: 1px solid var(--clr-neutral-300); + + /* Conservative fallback for the height of the sticky top navbar. + The actual measured height is published by static/js/navbar-height.js + and overrides this value on load. Other sticky elements (e.g. + .param-table thead) read --navbar-height to park flush below it. */ + --navbar-height: 5rem; } $user: '\01F464'; diff --git a/docs/assets/sass/base/_base.scss b/docs/assets/sass/base/_base.scss index d8acd6b108..73d50321b1 100644 --- a/docs/assets/sass/base/_base.scss +++ b/docs/assets/sass/base/_base.scss @@ -35,6 +35,7 @@ code { pre code { display: block; padding: 1em; + background-color: transparent; line-height: 2; overflow-x: auto; } @@ -64,4 +65,4 @@ pre { margin-top: 3rem; } } -} \ No newline at end of file +} diff --git a/docs/assets/sass/base/_syntax.scss b/docs/assets/sass/base/_syntax.scss index 2969abb10a..2fcb7d79b5 100644 --- a/docs/assets/sass/base/_syntax.scss +++ b/docs/assets/sass/base/_syntax.scss @@ -1,6 +1,6 @@ /* Background */ .chroma { - background-color: #ffffff; + background-color: transparent; } /* Other */ @@ -429,4 +429,4 @@ /* TextWhitespace */ .chroma .w { color: #bbbbbb -} \ No newline at end of file +} diff --git a/docs/assets/sass/base/_typography.scss b/docs/assets/sass/base/_typography.scss index ee0c4ebe49..3e455c83b4 100644 --- a/docs/assets/sass/base/_typography.scss +++ b/docs/assets/sass/base/_typography.scss @@ -55,7 +55,6 @@ a { &:hover, &:focus { - text-decoration: none; color: var(--clr-primary-300); } } diff --git a/docs/assets/sass/components/_badge.scss b/docs/assets/sass/components/_badge.scss new file mode 100644 index 0000000000..f291f602fd --- /dev/null +++ b/docs/assets/sass/components/_badge.scss @@ -0,0 +1,43 @@ +.badge { + display: inline-flex; + align-items: center; + gap: var(--space-3xs); + + padding: 0.1em 0.5em; + border-radius: 999px; + + font-size: var(--fs-300); + font-weight: 600; + line-height: 1.4; + white-space: nowrap; + + background: var(--clr-neutral-200); + color: var(--clr-neutral-700); + + /* Version badges — readable text, never strikethrough. */ + &--since { + background: var(--clr-success-100); + color: var(--clr-success-500); + } + + &--deprecated-in { + background: var(--clr-warning-100); + color: var(--clr-warning-500); + } + + &--hidden-in { + background: var(--clr-danger-100); + color: var(--clr-danger-500); + } + + /* Legacy aliases — used outside the param-table (since.html, deprecated.html). */ + &--deprecated { + background: var(--clr-warning-100); + color: var(--clr-warning-500); + } + + &--removed { + background: var(--clr-danger-100); + color: var(--clr-danger-500); + } +} diff --git a/docs/assets/sass/components/_button.scss b/docs/assets/sass/components/_button.scss index 68346cdbe1..9dee74ae80 100644 --- a/docs/assets/sass/components/_button.scss +++ b/docs/assets/sass/components/_button.scss @@ -1,36 +1,60 @@ +/* The .btn classes apply directly to either a ` {{ end }} {{ end }} -{{ end }} \ No newline at end of file +{{ end }} diff --git a/docs/layouts/index.html b/docs/layouts/index.html index 1136f18925..e48d449c16 100644 --- a/docs/layouts/index.html +++ b/docs/layouts/index.html @@ -1,10 +1,9 @@ {{ define "main" }}
{{ partial "home/hero.html" . }} - {{ partial "home/info.html" . }} - {{ partial "home/feature.html" . }} - {{ partial "home/example.html" . }} - {{ partial "home/report.html" . }} - {{ partial "home/support.html" . }} +
+ {{ partial "home/quick-install.html" . }} + {{ partial "home/release.html" . }} +
-{{ end }} \ No newline at end of file +{{ end }} diff --git a/docs/layouts/index.searchindex.json b/docs/layouts/index.searchindex.json index dff377319e..76fcea0d22 100644 --- a/docs/layouts/index.searchindex.json +++ b/docs/layouts/index.searchindex.json @@ -1,10 +1,71 @@ +{{- /* + Search index consumed by static/js/lunr-search.js. + + Two kinds of entries are emitted: + 1. One entry per regular page (title, plain text body, description, categories). + 2. One entry per connection property from data/connection-properties.yaml, + so the search picks up property names directly and deep-links into the + reference table via #prop-. +*/ -}} [ - {{- range $index, $page := .Site.RegularPages -}} - {{- if gt $index 0 -}} , {{- end -}} - {{- $entry := dict "uri" $page.RelPermalink "title" $page.Title -}} - {{- $entry = merge $entry (dict "content" ($page.Plain | htmlUnescape)) -}} - {{- $entry = merge $entry (dict "description" $page.Description) -}} - {{- $entry = merge $entry (dict "categories" $page.Params.categories) -}} - {{- $entry | jsonify -}} +{{- $first := true -}} +{{- range .Site.RegularPages -}} + {{- if not $first -}},{{- end -}}{{- $first = false -}} + {{/* Strip the {{< review >}} shortcode's output before plainifying so that + file paths and line numbers inside review entries (e.g. PGProperty.java) + don't match unrelated search queries and don't leak into result + snippets as "Reviewed YYYY-MM-DD against source: ...". */}} + {{- $body := replaceRE `

[\s\S]*?

\s*` "" .Content -}} + {{- $entry := dict + "uri" .RelPermalink + "title" .Title + "content" ($body | plainify | htmlUnescape) + "description" .Description + "categories" .Params.categories + -}} + {{- $entry | jsonify -}} +{{- end -}} +{{/* Resolve the connection-properties reference page through Hugo so its + URL carries the configured basePath (e.g. /pgjdbc/ on GitHub Pages + forks). RelPermalink is `/documentation/.../` for the production site + and `/pgjdbc/documentation/.../` for forks — and updates automatically + if the page is ever moved. */}} +{{- $propsPage := .Site.GetPage "/documentation/reference/connection-properties" -}} +{{- $propsRef := $propsPage.RelPermalink -}} +{{- range index .Site.Data "connection-properties" -}} + {{- $status := upper (.status | default "STABLE") -}} + {{/* Skip HIDDEN properties: the reference table renders them but they're + deliberately not advertised to users, so they don't belong in search. */}} + {{- if ne $status "HIDDEN" -}} + {{- if not $first -}},{{- end -}}{{- $first = false -}} + {{- $anchor := anchorize .name -}} + {{- $desc := .description | default "" | plainify | htmlUnescape -}} + {{- $tags := .tags | default slice -}} + {{- $versionBits := slice -}} + {{- with .introducedIn -}}{{- $versionBits = $versionBits | append (printf "since %s" .) -}}{{- end -}} + {{- with .deprecatedIn -}}{{- $versionBits = $versionBits | append (printf "deprecated in %s" .) -}}{{- end -}} + {{- $statusPhrase := "" -}} + {{- if and (ne $status "STABLE") (ne $status "") -}} + {{- $statusPhrase = lower $status -}} {{- end -}} -] \ No newline at end of file + {{/* Content packs the property name (so partial matches light up the body + field too), description, tags, version markers, default value and + status into one string. camelCase splitting is done by the JS-side + identifier-aware tokenizer in lunr-search.js, so no Hugo-side + n-gram expansion is needed here. */}} + {{- $contentBits := slice .name $desc -}} + {{- with $tags -}}{{- $contentBits = $contentBits | append (delimit . " ") -}}{{- end -}} + {{- with $versionBits -}}{{- $contentBits = $contentBits | append (delimit . " ") -}}{{- end -}} + {{- with .default -}}{{- $contentBits = $contentBits | append (printf "default %s" .) -}}{{- end -}} + {{- with $statusPhrase -}}{{- $contentBits = $contentBits | append . -}}{{- end -}} + {{- $entry := dict + "uri" (printf "%s#prop-%s" $propsRef $anchor) + "title" .name + "content" (delimit $contentBits " — ") + "description" $desc + "categories" $tags + -}} + {{- $entry | jsonify -}} + {{- end -}} +{{- end -}} +] diff --git a/docs/layouts/partials/docs/article.html b/docs/layouts/partials/docs/article.html index 9a9b7f9855..2242437e4b 100644 --- a/docs/layouts/partials/docs/article.html +++ b/docs/layouts/partials/docs/article.html @@ -1,10 +1,56 @@
+ {{- partial "docs/page-meta.html" . -}}

{{if eq .Title "Documentation"}} - document + document {{end}} {{ .Title }}

{{ .Content }} + + {{/* On section pages, render a card list of child pages + (subsections and regular pages alike, ordered by weight). + Without this, readers landing on /documentation/connect/ + see only the section's short intro paragraph and have to + retreat to the sidebar to continue. */}} + {{- if .IsSection }} + {{- with .Pages.ByWeight }} +
+ {{- end }} + {{- end }} + {{- partial "previous-next-links.html" . -}}
\ No newline at end of file diff --git a/docs/layouts/partials/docs/page-meta.html b/docs/layouts/partials/docs/page-meta.html new file mode 100644 index 0000000000..b16612faf0 --- /dev/null +++ b/docs/layouts/partials/docs/page-meta.html @@ -0,0 +1,56 @@ +{{/* + page-meta: renders a thin band above the article body containing + breadcrumbs, last_reviewed date, and an "Edit this page on GitHub" link. + + Reads frontmatter fields: + last_reviewed string (ISO date), optional + + Reads Site.Params: + docsRepo (string) github repo URL, e.g. "https://github.com/pgjdbc/pgjdbc" + docsBranch (string) branch name on the repo + docsContentRoot (string) path within the repo where the Hugo content/ lives + + Breadcrumbs walk up Section -> Section.Parent -> ... -> home. +*/}} +{{- $home := .Site.Home -}} +{{- $crumbs := slice -}} +{{- $section := .CurrentSection -}} +{{- range seq 8 -}}{{/* bounded loop to climb up the section tree */}} + {{- if and $section (ne $section.RelPermalink "/") -}} + {{- $crumbs = $crumbs | append $section -}} + {{- $section = $section.Parent -}} + {{- end -}} +{{- end -}} +{{- $reversed := slice -}} +{{- range $i, $c := $crumbs -}} + {{- $reversed = $reversed | append (index $crumbs (sub (sub (len $crumbs) 1) $i)) -}} +{{- end -}} +
+
    +
  1. Home
  2. + {{- range $reversed }} +
  3. {{ .Title }}
  4. + {{- end }} + {{- if and .CurrentSection (ne .RelPermalink .CurrentSection.RelPermalink) }} +
  5. {{ .Title }}
  6. + {{- end }} +
+ + {{- with .Params.last_reviewed }} + + Last reviewed + + {{- end }} + + {{- $repo := .Site.Params.docsRepo -}} + {{- $branch := .Site.Params.docsBranch | default "master" -}} + {{- $root := .Site.Params.docsContentRoot | default "docs/content" -}} + {{- with .File }} + {{- if $repo }} + + Edit this page on GitHub + + {{- end }} + {{- end }} +
diff --git a/docs/layouts/partials/docs/sidebar.html b/docs/layouts/partials/docs/sidebar.html index 0dead43485..05c8dcd719 100644 --- a/docs/layouts/partials/docs/sidebar.html +++ b/docs/layouts/partials/docs/sidebar.html @@ -1,13 +1,60 @@ -{{ $currentPage := . }} +{{/* + Two-level documentation sidebar driven by the actual page tree under + /documentation/, not by menus.toml. - diff --git a/docs/layouts/partials/home/example.html b/docs/layouts/partials/home/example.html deleted file mode 100644 index 6f9edf3b3e..0000000000 --- a/docs/layouts/partials/home/example.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

Processing a simple query

- {{.Content}} -
\ No newline at end of file diff --git a/docs/layouts/partials/home/feature.html b/docs/layouts/partials/home/feature.html deleted file mode 100644 index fa81d06ad7..0000000000 --- a/docs/layouts/partials/home/feature.html +++ /dev/null @@ -1,8 +0,0 @@ -
- {{ range $.Site.Data.homepagedata.feature }} -
- feature -

{{ .desc }}

-
- {{ end }} -
\ No newline at end of file diff --git a/docs/layouts/partials/home/hero.html b/docs/layouts/partials/home/hero.html index bd5e41d6a7..59f98e60b2 100644 --- a/docs/layouts/partials/home/hero.html +++ b/docs/layouts/partials/home/hero.html @@ -1,21 +1,41 @@
- elephant + -
\ No newline at end of file + diff --git a/docs/layouts/partials/home/info.html b/docs/layouts/partials/home/info.html deleted file mode 100644 index 5cc7e2a66d..0000000000 --- a/docs/layouts/partials/home/info.html +++ /dev/null @@ -1,29 +0,0 @@ -
-
-

- Why pgJDBC? -

-

- The PostgreSQL JDBC Driver allows Java programs to connect to a PostgreSQL database using standard, database - independent Java code. pgJDBC is an open source JDBC driver written in Pure Java (Type 4), and communicates in the - PostgreSQL native network protocol. Because of this, the driver is platform independent; once compiled, the - driver can be used on any system. -

-
-
-

- Latest Releases -

-

- Current release is 42.7.11 This is a security and maintenance release. This fixes a SCRAM PBKDF2 iteration DoS vulnerability (CVE-2026-42198), adds the require_auth connection property, and includes numerous bug fixes. - See the Changelog for details -

-
    - {{ range $.Site.Data.homepagedata.info }} -
  • - {{ .version }} · {{ .date }} · Notes -
  • - {{ end}} -
-
-
diff --git a/docs/layouts/partials/home/quick-install.html b/docs/layouts/partials/home/quick-install.html new file mode 100644 index 0000000000..ec73a4a7b7 --- /dev/null +++ b/docs/layouts/partials/home/quick-install.html @@ -0,0 +1,76 @@ +{{/* + Quick install block — Maven + Gradle examples stacked rather + than tabbed. The previous tab UI hid two of three snippets + behind clicks; this layout shows both Maven and Gradle in one + scan. Only Kotlin DSL is shown for Gradle — the Groovy DSL + differs by `()` vs `''` quoting, which Groovy-DSL users adapt + trivially. + + Both blocks reuse the `code-tabs` markup (with a single tab + acting as the label) so the existing components/_code-tabs.scss + styling and static/js/code-tabs.js copy-button handling pick + this up unchanged. + + The version string is the same "latest release" the release + card shows, read from /changelogs/*-release.md sorted reverse- + chronological. Single source of truth shared with release.html. +*/}} +{{- $version := "" -}} +{{- range (where site.RegularPages "Section" "changelogs").ByDate.Reverse -}} + {{- if and (not $version) (findRE "^[0-9]" (.Params.version | default "")) -}} + {{- $version = .Params.version -}} + {{- end -}} +{{- end -}} +{{- $maven := printf ` + org.postgresql + postgresql + %s +` $version -}} +{{- $gradleKotlin := printf `implementation("org.postgresql:postgresql:%s")` $version -}} + +
+

Add it to your build

+ + {{/* Snippets share a single grid track sized to the widest + block (Maven XML), so the Gradle one-liner stretches to + match instead of leaving a stepped right edge. */}} +
+
+
    + +
+
+ +
+ {{ transform.Highlight $maven "xml" "" }} +
+
+
+ +
+
    + +
+
+ +
+ {{ transform.Highlight $gradleKotlin "kotlin" "" }} +
+
+
+
+
diff --git a/docs/layouts/partials/home/release.html b/docs/layouts/partials/home/release.html new file mode 100644 index 0000000000..76cbf434f2 --- /dev/null +++ b/docs/layouts/partials/home/release.html @@ -0,0 +1,54 @@ +{{/* + Current release card. + + Single source of truth for "the latest release" is the + /changelogs/ section's content tree. Each release file's + front-matter carries `version`, `date`, and `summary`; the + permalink is the URL. Listing in `.ByDate.Reverse` and taking + the first entry whose version starts with a digit (so any + non-release status notices filed in the section in the past + are skipped) gives the same data the homepage used to read from + homepagedata.toml, minus the duplication. + + When a new version ships, only one file changes: the new + /changelogs/--release.md. The homepage + picks it up automatically on the next build. +*/}} +{{- $releases := slice -}} +{{- range (where site.RegularPages "Section" "changelogs").ByDate.Reverse -}} + {{- if findRE "^[0-9]" (.Params.version | default "") -}} + {{- $releases = $releases | append . -}} + {{- end -}} +{{- end -}} +{{- $latest := index $releases 0 -}} +{{- $recent := first 3 (after 1 $releases) -}} + +
+

Latest release

+ +
+
+ {{ $latest.Params.version }} + {{ $latest.Date.Format "2 January 2006" }} +
+ + {{- with $latest.Params.summary }} +

{{ . }}

+ {{- end }} + + + + {{- if $recent }} +

+ Recent: + {{- range $i, $r := $recent }} + {{ if $i }}·{{ end }} + {{ $r.Params.version }} + {{- end }} +

+ {{- end }} +
+
diff --git a/docs/layouts/partials/home/report.html b/docs/layouts/partials/home/report.html deleted file mode 100644 index c315bd0a28..0000000000 --- a/docs/layouts/partials/home/report.html +++ /dev/null @@ -1,23 +0,0 @@ -
-

Report a bug or Contribute ?

-
- - -
-
\ No newline at end of file diff --git a/docs/layouts/partials/home/support.html b/docs/layouts/partials/home/support.html deleted file mode 100644 index 593fca09b2..0000000000 --- a/docs/layouts/partials/home/support.html +++ /dev/null @@ -1,9 +0,0 @@ -
- support -
-

Support Us

-

PostgreSQL is free.
Please support our work by making a - donation. -

-
-
\ No newline at end of file diff --git a/docs/layouts/partials/nav-mobile.html b/docs/layouts/partials/nav-mobile.html index e4dbb86bb2..93bf6f247e 100644 --- a/docs/layouts/partials/nav-mobile.html +++ b/docs/layouts/partials/nav-mobile.html @@ -2,21 +2,32 @@
+ {{/* Mirror the desktop sidebar: section landings + their pages, + driven by the page tree instead of menus.toml. */}} + {{- $docsRoot := .Site.GetPage "/documentation" -}}
    - {{ range .Site.Menus.docs.ByWeight }} + {{- range $docsRoot.RegularPages.ByWeight }}
  • - - {{.Name }} - + {{ .Title }}
  • - {{- end}} + {{- end }} + {{- range $docsRoot.Sections.ByWeight }} +
  • + {{ .Title }} +
  • + {{- range .RegularPages.ByWeight }} +
  • + {{ .Title }} +
  • + {{- end }} + {{- end }}