Fix blockers found by a cold-start test of the published page (#160) #79
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |