Skip to content

Fix blockers found by a cold-start test of the published page (#160) #79

Fix blockers found by a cold-start test of the published page (#160)

Fix blockers found by a cold-start test of the published page (#160) #79

# Workflow for building Next.js site and downloading DocumentDB packages, then deploying to GitHub Pages
name: Deploy Next.js site and DocumentDB packages to Pages
on:
# Runs on pushes targeting the default branch
push:
branches:
- main
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: pages
cancel-in-progress: false
jobs:
# Build job
build:
name: Build Next.js static site
# Sets permissions of the GITHUB_TOKEN to allow reading of repository content
permissions:
contents: read
runs-on: ubuntu-22.04
# Without an explicit timeout a stalled step runs against GitHub's 6-hour
# default before failing, which for `pages` concurrency means blocking
# every deployment queued behind it.
timeout-minutes: 30
steps:
- name: Checkout source
uses: actions/checkout@v7
- name: Resolve optional build features
# The static site is always built. Mirroring the release packages and
# signing them are separate opt-outs so that a fork with no secrets can
# still run this workflow end to end.
id: features
env:
# The `secrets` context is not readable from a step-level `if:`, so
# the presence of the signing key has to be resolved into a step
# output first. Binding the secret to this one step also keeps it out
# of every other step's environment.
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
# A fork has neither the signing key nor a reason to spend several
# minutes mirroring release assets, so package generation defaults
# off outside this repository. `BUILD_PACKAGES` overrides it either
# way - but note that turning it off upstream publishes a site with
# no /deb, /rpm and no keyring, which breaks `apt-get update` for
# everyone already pointed at the repository.
BUILD_PACKAGES: ${{ vars.BUILD_PACKAGES }}
IS_UPSTREAM: ${{ github.repository == 'documentdb/documentdb.github.io' }}
run: |
set -euo pipefail
requested=$(printf '%s' "$BUILD_PACKAGES" | tr '[:upper:]' '[:lower:]')
case "$requested" in
true|false) packages="$requested" ;;
'') packages="$IS_UPSTREAM" ;;
*)
echo "::error::BUILD_PACKAGES must be 'true' or 'false' (got '$BUILD_PACKAGES')"
exit 1
;;
esac
if [ "$packages" = 'true' ] && [ -n "$GPG_PRIVATE_KEY" ]; then
sign=true
else
sign=false
fi
echo "packages=$packages" >> "$GITHUB_OUTPUT"
echo "sign=$sign" >> "$GITHUB_OUTPUT"
{
echo "### Build configuration"
echo ""
echo "| Feature | Enabled | Controlled by |"
echo "| --- | --- | --- |"
echo "| Static site | true | always built |"
echo "| Package repositories | $packages | \`BUILD_PACKAGES\` variable |"
echo "| Package signing | $sign | \`GPG_PRIVATE_KEY\` secret |"
} >> "$GITHUB_STEP_SUMMARY"
if [ "$packages" = 'true' ] && [ "$sign" != 'true' ]; then
echo "::warning::GPG_PRIVATE_KEY is not set - the package repositories will be published unsigned."
fi
- name: Install packaging tools
if: steps.features.outputs.packages == 'true'
run: |
until sudo apt-get update; do sleep 1; done
sudo apt-get install -y createrepo-c dpkg-dev dpkg-sig gnupg2 python3
- name: Setup GPG
id: import_gpg
if: steps.features.outputs.sign == 'true'
# Deliberately no `continue-on-error`: a key that is configured but
# cannot be imported has to fail the run. Swallowing that error
# republishes the site with an unsigned repository in place of a signed
# one, which breaks `apt-get update` for every client pinned with
# `signed-by`. Signing is optional; silently losing it is not.
uses: crazy-max/ghaction-import-gpg@v7
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
- name: Configure package build
if: steps.features.outputs.packages == 'true'
env:
SIGN: ${{ steps.features.outputs.sign }}
FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
KEY_ID: ${{ steps.import_gpg.outputs.keyid }}
KEY_NAME: ${{ steps.import_gpg.outputs.name }}
KEY_EMAIL: ${{ steps.import_gpg.outputs.email }}
# Configure which DocumentDB release to mirror. Both can be
# overridden by repository variables.
DOCUMENTDB_VERSION: ${{ vars.DOCUMENTDB_VERSION || 'latest' }}
MULTI_VERSION: ${{ vars.MULTI_VERSION || 'true' }}
run: |
set -euo pipefail
if [ "$SIGN" = 'true' ]; then
if [ -z "$FINGERPRINT" ]; then
echo "::error::The GPG key imported without a fingerprint; refusing to publish an unsigned repository."
exit 1
fi
echo "GPG_FINGERPRINT=$FINGERPRINT" >> "$GITHUB_ENV"
echo "GPG key loaded successfully"
echo " Fingerprint: $FINGERPRINT"
echo " Key ID: $KEY_ID"
echo " User ID: $KEY_NAME <$KEY_EMAIL>"
else
echo "No GPG key configured - packages will not be signed."
echo "To enable signing, add GPG_PRIVATE_KEY to the repository secrets."
fi
echo "DOCUMENTDB_VERSION=$DOCUMENTDB_VERSION" >> "$GITHUB_ENV"
echo "MULTI_VERSION=$MULTI_VERSION" >> "$GITHUB_ENV"
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3
bundler-cache: true
- name: Restore cache
uses: actions/cache@v6
with:
path: |
.next/cache
# Generate a new cache whenever packages or source files change.
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
# If source files changed but packages didn't, rebuild from a prior cache.
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-
- name: Install dependencies
run: npm ci
- name: Build with Next.js
# This repository is the organization Pages site served at the root of
# the custom domain (documentdb.io), so the build must NOT set
# NEXT_BASE_PATH. Setting it to the repository name (the usual trick
# for project pages) prefixes every internal link and asset URL with
# /documentdb.github.io/, which GitHub Pages then 301-redirects back
# to the root on every request and leaves the prefixed URL visible in
# the address bar after client-side navigation.
env:
JEKYLL_BASE_PATH: /blogs
run: npm run build
- name: Verify exported documentation pages
# A partially failed content compile must never reach production as a
# docs-less site. compile-content fails the build on clone/copy errors;
# this is the independent belt-and-braces check on the final artifact.
run: |
set -euo pipefail
for page in out/index.html out/docs/index.html out/docs/getting-started/index.html out/docs/reference/index.html; do
if [ ! -f "$page" ]; then
echo "Missing expected page: $page"
exit 1
fi
done
reference_count=$(find out/docs/reference -name index.html | wc -l)
echo "Reference pages exported: $reference_count"
# The docs repo currently holds ~240 reference entries; well under
# half of that means the compile silently lost content.
if [ "$reference_count" -lt 100 ]; then
echo "Only $reference_count reference pages exported - documentation content looks incomplete."
exit 1
fi
- name: Download DocumentDB packages from latest release
if: steps.features.outputs.packages == 'true'
run: .github/scripts/download_packages.sh
- name: Verify generated package components
if: steps.features.outputs.packages == 'true'
env:
SIGN: ${{ steps.features.outputs.sign }}
run: |
set -euo pipefail
python3 - <<'PY'
import json
import os
from pathlib import Path
release_info = Path("out/packages/release-info.json")
if not release_info.exists():
raise SystemExit("release-info.json was not generated")
data = json.loads(release_info.read_text())
assets = [asset["name"] for asset in data.get("assets", [])]
components = ("deb11", "deb12", "deb13", "ubuntu22", "ubuntu24")
for component in components:
has_assets = any(
name.endswith(".deb")
and (
name.startswith(f"{component}-")
or name.startswith(f"{component}.04-")
)
for name in assets
)
if not has_assets:
continue
for arch in ("amd64", "arm64"):
packages = Path(f"out/deb/dists/stable/{component}/binary-{arch}/Packages")
packages_gz = Path(f"out/deb/dists/stable/{component}/binary-{arch}/Packages.gz")
if not packages.exists() or not packages_gz.exists():
raise SystemExit(
f"Missing APT metadata for {component} {arch}: "
f"{packages} / {packages_gz}"
)
release_file = Path("out/deb/dists/stable/Release")
if release_file.exists() and any(name.startswith("deb13-") and name.endswith(".deb") for name in assets):
release_text = release_file.read_text()
if "deb13" not in release_text:
raise SystemExit("deb13 assets exist but deb13 is missing from the APT Release file")
# A run that imported a signing key must not publish an unsigned
# repository: apt rejects a suite whose InRelease/Release.gpg vanished,
# so a silently skipped signature is a client-visible outage rather
# than a cosmetic regression.
if os.environ.get("SIGN") == "true":
if any(name.endswith(".deb") for name in assets):
for artifact in (
Path("out/deb/dists/stable/Release.gpg"),
Path("out/deb/dists/stable/InRelease"),
Path("out/documentdb-archive-keyring.gpg"),
):
if not artifact.exists():
raise SystemExit(
f"Signing was enabled but {artifact} was not produced"
)
# RPM metadata signing is best-effort inside the download script,
# so surface it as a warning instead of failing the deployment.
for repomd in sorted(Path("out/rpm").glob("*/repodata/repomd.xml")):
if not Path(f"{repomd}.asc").exists():
print(f"::warning::{repomd} was not signed")
PY
# The metadata checks above prove the repository FILES exist. They do not
# prove the repository is INSTALLABLE, and that gap has already shipped a
# broken repository once: when v0.116-0 introduced the multi-package
# layout, the asset filter still matched only `postgresql-N-documentdb`,
# so 14 of 22 packages - including the `documentdb` meta package and
# everything it depends on - were silently dropped. Every file the
# verifier looks for was present and correct; `apt install documentdb`
# was simply unsatisfiable.
#
# So resolve the real thing against the repository we are about to
# publish. This is dependency RESOLUTION only (`apt-get -s`, `dnf
# --assumeno`): it reads the indexes and solves the graph without
# downloading ~500 MB of PostgreSQL and PostGIS, which keeps the check to
# a few seconds while still catching the entire "package missing from the
# pool / unsatisfiable dependency" class.
- name: Verify the site's fallback release matches the mirrored release
if: steps.features.outputs.packages == 'true'
run: node .github/scripts/check_release_drift.js
- name: Smoke test the generated repository (dependency resolution)
if: steps.features.outputs.packages == 'true'
run: |
set -euo pipefail
# The pool is only meaningful if the meta package is actually in it.
# Fail loudly rather than "passing" a repository that has nothing to
# resolve.
if ! ls out/deb/pool/ubuntu24/documentdb_*_all.deb >/dev/null 2>&1; then
echo "::error::out/deb/pool/ubuntu24 has no documentdb meta package - the APT pool is incomplete"
exit 1
fi
if ! ls out/rpm/rhel9/documentdb-*.noarch.rpm >/dev/null 2>&1; then
echo "::error::out/rpm/rhel9 has no documentdb meta package - the RPM pool is incomplete"
exit 1
fi
python3 -m http.server 8099 --directory out >/tmp/repo-http.log 2>&1 &
HTTP_PID=$!
trap 'kill "$HTTP_PID" 2>/dev/null || true' EXIT
for _ in $(seq 1 30); do
curl -fsS -o /dev/null "http://127.0.0.1:8099/deb/dists/stable/Release" && break
sleep 1
done
echo "::group::APT resolution (ubuntu24 component)"
# --network host so the container reaches the runner's http.server.
# [trusted=yes] because this local mirror is unsigned when the
# deployment runs without a signing key; signature verification is
# asserted separately by the "Verify generated package components"
# step above.
docker run --rm --network host ubuntu:24.04 bash -c '
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq >/dev/null
apt-get install -y -qq curl ca-certificates gnupg lsb-release >/dev/null
install -d /usr/share/postgresql-common/pgdg
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| gpg --dearmor -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.gpg] https://apt.postgresql.org/pub/repos/apt noble-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
echo "deb [trusted=yes] http://127.0.0.1:8099/deb stable ubuntu24" \
> /etc/apt/sources.list.d/documentdb.list
apt-get update -qq
# -s solves the dependency graph and exits non-zero if it cannot.
apt-get install -s documentdb > /tmp/sim.txt
# Resolving is necessary but not sufficient: assert the transaction
# actually pulls the stack, so a meta package that degenerated into
# an empty shell cannot pass.
for pkg in documentdb documentdb-common documentdb-gateway documentdb-postgresql-tools; do
grep -q "Inst $pkg " /tmp/sim.txt || { echo "APT plan is missing $pkg"; cat /tmp/sim.txt; exit 1; }
done
grep -qE "Inst postgresql-1[78]-documentdb " /tmp/sim.txt || { echo "APT plan pulls no extension package"; exit 1; }
echo "APT OK: $(grep -c "^Inst " /tmp/sim.txt) packages in the plan"
'
echo "::endgroup::"
echo "::group::DNF resolution (rhel9 pool)"
docker run --rm --network host rockylinux/rockylinux:9 bash -c '
set -e
dnf install -y -q dnf-plugins-core >/dev/null 2>&1
dnf install -y -q https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm >/dev/null 2>&1
dnf install -y -q epel-release >/dev/null 2>&1
dnf config-manager --set-enabled crb
dnf -qy module disable postgresql >/dev/null 2>&1
printf "%s\n" "[documentdb]" "name=DocumentDB" "baseurl=http://127.0.0.1:8099/rpm/rhel9" "enabled=1" "gpgcheck=0" \
> /etc/yum.repos.d/documentdb.repo
# --assumeno always exits non-zero (the answer is "no"), so judge the
# transaction it printed rather than the exit code.
dnf install --assumeno documentdb > /tmp/sim.txt 2>&1 || true
if grep -qE "^Error|nothing provides|Problem:" /tmp/sim.txt; then
echo "DNF could not resolve documentdb:"; cat /tmp/sim.txt; exit 1
fi
for pkg in documentdb-common documentdb-gateway documentdb-postgresql-tools; do
grep -q "$pkg" /tmp/sim.txt || { echo "DNF plan is missing $pkg"; cat /tmp/sim.txt; exit 1; }
done
grep -qE "postgresql1[78]-documentdb" /tmp/sim.txt || { echo "DNF plan pulls no extension package"; exit 1; }
echo "DNF OK"
'
echo "::endgroup::"
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
with:
path: ./out
# Deployment job
deploy:
name: Publish site to GitHub Pages
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
timeout-minutes: 10
needs:
- build
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
pages: write
id-token: write
steps:
- name: Setup Pages
uses: actions/configure-pages@v6
with:
# Automatically inject basePath in your Next.js configuration file and disable
# server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized).
#
# You may remove this line if you want to manage the configuration yourself.
static_site_generator: next
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5