Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 12 additions & 38 deletions .github/scripts/download_packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ set -e
REPO="documentdb/documentdb"
OUT_DIR="out"
DOCUMENTDB_VERSION="${DOCUMENTDB_VERSION:-latest}"
MULTI_VERSION="${MULTI_VERSION:-true}"
SUITE="${SUITE:-stable}"
COMPONENTS="${COMPONENTS:-main}"
ORIGIN="${ORIGIN:-DocumentDB}"
Expand Down Expand Up @@ -76,34 +75,20 @@ echo "Downloading packages from $REPO releases"
# ---------------------------------------------------------------------------
# Release selection
#
# The primary release supplies the site's release-info.json and is the version
# users are told about. But a release only ships the distributions that were
# in its own build matrix: v0.116-0, for example, ships Tier-1 (ubuntu24 +
# rhel9) only, while v0.114-0 shipped seven distributions. Rebuilding the
# repository from the primary release alone would therefore DELETE the
# deb11/deb12/deb13/ubuntu22 components and the whole rhel8 repository from
# documentdb.io, and every host already pointed at one of them would start
# failing `apt update` with "Component 'ubuntu22' is not defined". That is a
# client-visible outage, not a cosmetic regression.
#
# So the repository is built additively: the primary release fills every
# distribution it ships, then progressively older releases are consulted ONLY
# to fill distributions still missing. A distribution is claimed by the newest
# release that ships it and is never overwritten by an older one. Once a
# release ships every distribution again, the older ones stop contributing
# by themselves - no cleanup required.
# The selected release is the package repository's single source of truth.
# Older releases must not fill gaps: those combinations are on-demand builds,
# not assets of the current official release, and mixing them into the pool
# makes stale versions look supported.
# ---------------------------------------------------------------------------
MAX_RELEASES="${MAX_RELEASES:-8}"

RELEASES_JSON=$(mktemp)
if ! curl -fqs "https://api.github.com/repos/${REPO}/releases?per_page=100" > "$RELEASES_JSON"; then
echo "Error: Could not fetch release list"
exit 1
fi

# Ordered list of tags to consider, newest first. Drafts and prereleases are
# skipped: they are not what a repository-backed `apt install` should serve.
TAG_LIST=$(DOCUMENTDB_VERSION="$DOCUMENTDB_VERSION" python3 - "$RELEASES_JSON" <<'PY'
# Select exactly one published release. Drafts and prereleases are skipped.
SELECTED_TAG=$(DOCUMENTDB_VERSION="$DOCUMENTDB_VERSION" python3 - "$RELEASES_JSON" <<'PY'
import json, os, sys

releases = json.load(open(sys.argv[1]))
Expand All @@ -113,26 +98,17 @@ if not published:

requested = os.environ.get("DOCUMENTDB_VERSION", "latest")
if requested != "latest":
# The API returns releases newest-first.
index = next((i for i, r in enumerate(published)
if r["tag_name"] == requested), None)
if index is None:
selected = next((r for r in published if r["tag_name"] == requested), None)
if selected is None:
sys.exit(f"Error: Version {requested} not found in releases")
# A pin means "serve this version". Only the pinned release and OLDER ones
# may contribute: pulling gap-fillers from NEWER releases would defeat the
# pin, and worse, it would mix releases that depend on each other. Pinning
# to a release that predates the multi-package layout would otherwise add a
# newer `documentdb` meta package whose `documentdb-N (>= X)` dependency the
# pinned extension cannot satisfy - an unsatisfiable repository.
ordered = published[index:]
else:
ordered = published
selected = published[0]

print("\n".join(r["tag_name"] for r in ordered))
print(selected["tag_name"])
PY
)

PRIMARY_TAG=$(printf '%s\n' "$TAG_LIST" | head -n 1)
PRIMARY_TAG="$SELECTED_TAG"
echo "Primary release: $PRIMARY_TAG"

# Packages already placed, keyed by "pool|name|arch". A newer release always
Expand Down Expand Up @@ -223,9 +199,7 @@ rpm_pool_for() {

mkdir -p out/packages

for tag in $TAG_LIST; do
MAX_RELEASES=$((MAX_RELEASES - 1))
[ "$MAX_RELEASES" -lt 0 ] && break
for tag in "$PRIMARY_TAG"; do

if ! release=$(curl -fqs "https://api.github.com/repos/${REPO}/releases/tags/$tag"); then
echo "::warning::Could not fetch release $tag, skipping"
Expand Down
158 changes: 158 additions & 0 deletions .github/scripts/verify_package_inventory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Verify that the published pools contain only the selected release assets."""

import json
import re
from pathlib import Path


ROOT = Path("out")
RELEASE_INFO = ROOT / "packages" / "release-info.json"

DEB_PREFIXES = {
"deb11-": "deb11",
"deb12-": "deb12",
"deb13-": "deb13",
"ubuntu22.04-": "ubuntu22",
"ubuntu24.04-": "ubuntu24",
}
RPM_POOLS = ("rhel8", "rhel9")


def rpm_pool(name: str) -> str | None:
for pool in RPM_POOLS:
if name.startswith(f"{pool}-") or f".el{pool[-1]}." in name:
return pool
return None


def fail(message: str) -> None:
raise SystemExit(message)


if not RELEASE_INFO.exists():
fail(f"Missing {RELEASE_INFO}")

release = json.loads(RELEASE_INFO.read_text())
asset_names = {
asset["name"]
for asset in release.get("assets", [])
if isinstance(asset, dict) and isinstance(asset.get("name"), str)
}
package_assets = {
name
for name in asset_names
if (name.endswith(".deb") and "dbgsym" not in name)
or (
name.endswith(".rpm")
and "debuginfo" not in name
and "debugsource" not in name
)
}

downloaded = {
path.name
for path in (ROOT / "packages").iterdir()
if path.suffix in {".deb", ".rpm"}
}
if downloaded != package_assets:
fail(
"Direct package mirror does not match release assets.\n"
f"Missing: {sorted(package_assets - downloaded)}\n"
f"Unexpected: {sorted(downloaded - package_assets)}"
)

expected_deb: dict[str, set[str]] = {}
for name in sorted(package_assets):
if not name.endswith(".deb"):
continue
match = next(
((prefix, component) for prefix, component in DEB_PREFIXES.items() if name.startswith(prefix)),
None,
)
if match is None:
fail(f"Unrecognized DEB release asset: {name}")
prefix, component = match
expected_deb.setdefault(component, set()).add(name.removeprefix(prefix))

deb_pool_root = ROOT / "deb" / "pool"
actual_deb_components = {
path.name for path in deb_pool_root.iterdir() if path.is_dir()
} if deb_pool_root.exists() else set()
if actual_deb_components != set(expected_deb):
fail(
"APT components do not match the selected release.\n"
f"Expected: {sorted(expected_deb)}\n"
f"Actual: {sorted(actual_deb_components)}"
)

for component, expected in expected_deb.items():
actual = {path.name for path in (deb_pool_root / component).glob("*.deb")}
if actual != expected:
fail(
f"APT pool {component} does not match the selected release.\n"
f"Missing: {sorted(expected - actual)}\n"
f"Unexpected: {sorted(actual - expected)}"
)

release_file = ROOT / "deb" / "dists" / "stable" / "Release"
if expected_deb:
if not release_file.exists():
fail(f"Missing {release_file}")
match = re.search(r"^Components:\s*(.+)$", release_file.read_text(), re.MULTILINE)
components = set(match.group(1).split()) if match else set()
if components != set(expected_deb):
fail(
"APT Release components do not match the selected release.\n"
f"Expected: {sorted(expected_deb)}\n"
f"Actual: {sorted(components)}"
)

rpm_assets = {name for name in package_assets if name.endswith(".rpm")}
explicit_rpm_pools = {pool for name in rpm_assets if (pool := rpm_pool(name))}
expected_rpm: dict[str, set[str]] = {pool: set() for pool in explicit_rpm_pools}

for name in rpm_assets:
pool = rpm_pool(name)
if pool:
expected_rpm[pool].add(re.sub(r"^rhel[89]-", "", name))
elif name.endswith(".noarch.rpm"):
for target in explicit_rpm_pools:
expected_rpm[target].add(name)
else:
fail(f"Unrecognized RPM release asset: {name}")

rpm_root = ROOT / "rpm"
expected_rpm_dirs = set(expected_rpm)
if "rhel8" in expected_rpm:
expected_rpm_dirs.add("main")
actual_rpm_pools = {
path.name
for path in rpm_root.iterdir()
if path.is_dir()
} if rpm_root.exists() else set()
if actual_rpm_pools != expected_rpm_dirs:
fail(
"RPM pools do not match the selected release.\n"
f"Expected: {sorted(expected_rpm_dirs)}\n"
f"Actual: {sorted(actual_rpm_pools)}"
)

for pool, expected in expected_rpm.items():
actual = {path.name for path in (rpm_root / pool).glob("*.rpm")}
if actual != expected:
fail(
f"RPM pool {pool} does not match the selected release.\n"
f"Missing: {sorted(expected - actual)}\n"
f"Unexpected: {sorted(actual - expected)}"
)

if "main" in expected_rpm_dirs:
actual_main = {path.name for path in (rpm_root / "main").glob("*.rpm")}
if actual_main != expected_rpm["rhel8"]:
fail("Legacy RPM main pool does not match the selected release's RHEL 8 pool.")

print(
f"Package pools exactly match {release.get('tag_name', 'the selected release')}: "
f"{len(package_assets)} release assets."
)
7 changes: 2 additions & 5 deletions .github/workflows/continuous-deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,8 @@ jobs:
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.
# Configure which DocumentDB release to mirror.
DOCUMENTDB_VERSION: ${{ vars.DOCUMENTDB_VERSION || 'latest' }}
MULTI_VERSION: ${{ vars.MULTI_VERSION || 'true' }}
run: |
set -euo pipefail
if [ "$SIGN" = 'true' ]; then
Expand All @@ -124,9 +122,7 @@ jobs:
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:
Expand Down Expand Up @@ -189,6 +185,7 @@ jobs:
SIGN: ${{ steps.features.outputs.sign }}
run: |
set -euo pipefail
python3 .github/scripts/verify_package_inventory.py
python3 - <<'PY'
import json
import os
Expand Down
Loading