|
4 | 4 | import re |
5 | 5 | from typing import Any, cast |
6 | 6 | from urllib.parse import urlparse |
| 7 | +from uuid import uuid4 |
7 | 8 |
|
8 | 9 | from google.cloud.firestore import transactional |
9 | 10 |
|
10 | 11 | from database._client import get_firestore_client |
11 | 12 |
|
12 | 13 | CHANNELS_COLLECTION = "desktop_update_channels" |
13 | 14 | MANIFESTS_COLLECTION = "desktop_release_manifests" |
| 15 | +ROLLBACK_AUDITS_COLLECTION = "desktop_update_channel_rollback_audits" |
14 | 16 | VALID_CHANNELS = frozenset({"stable", "beta"}) |
15 | 17 | VALID_PLATFORMS = frozenset({"macos", "windows", "linux"}) |
16 | 18 | SHA40_RE = re.compile(r"^[0-9a-f]{40}$", re.IGNORECASE) |
@@ -182,6 +184,46 @@ def _build_channel_pointer( |
182 | 184 | } |
183 | 185 |
|
184 | 186 |
|
| 187 | +def _build_beta_rollback_pointer( |
| 188 | + current: dict[str, Any], |
| 189 | + manifest: dict[str, Any], |
| 190 | + *, |
| 191 | + release_id: str, |
| 192 | + expected_current_release_id: str, |
| 193 | + expected_generation: int, |
| 194 | + updated_at: datetime | None = None, |
| 195 | +) -> dict[str, Any]: |
| 196 | + """Build the sole permitted non-monotonic pointer transition: macOS beta rollback.""" |
| 197 | + if manifest["platform"] != "macos": |
| 198 | + raise ValueError("rollback target must be a macos release manifest") |
| 199 | + qualification = cast(dict[str, Any], manifest["qualification"]) |
| 200 | + if qualification.get("passed") is not True or str(qualification.get("tier", "")).upper() != "T2": |
| 201 | + raise ValueError("rollback target is missing passed T2 qualification evidence") |
| 202 | + |
| 203 | + current_release_id = current.get("release_id") |
| 204 | + if current_release_id != expected_current_release_id: |
| 205 | + raise ValueError( |
| 206 | + f"current release mismatch: expected {expected_current_release_id}, current {current_release_id or 'missing'}" |
| 207 | + ) |
| 208 | + current_generation = _generation(current.get("generation", 0)) |
| 209 | + if expected_generation != current_generation: |
| 210 | + raise ValueError(f"generation mismatch: expected {expected_generation}, current {current_generation}") |
| 211 | + |
| 212 | + current_build = _generation(current.get("build_number")) |
| 213 | + if release_id == current_release_id or manifest["build_number"] >= current_build: |
| 214 | + raise ValueError("rollback target must be an earlier qualified beta release") |
| 215 | + |
| 216 | + return { |
| 217 | + "platform": "macos", |
| 218 | + "channel": "beta", |
| 219 | + "release_id": release_id, |
| 220 | + "version": manifest["version"], |
| 221 | + "build_number": manifest["build_number"], |
| 222 | + "generation": current_generation + 1, |
| 223 | + "updated_at": updated_at or datetime.now(timezone.utc), |
| 224 | + } |
| 225 | + |
| 226 | + |
185 | 227 | @transactional |
186 | 228 | def _promote_channel_transaction( |
187 | 229 | transaction: Any, |
@@ -249,6 +291,92 @@ def promote_channel( |
249 | 291 | ) |
250 | 292 |
|
251 | 293 |
|
| 294 | +@transactional |
| 295 | +def _rollback_macos_beta_transaction( |
| 296 | + transaction: Any, |
| 297 | + pointer_ref: Any, |
| 298 | + manifest_ref: Any, |
| 299 | + audit_ref: Any, |
| 300 | + *, |
| 301 | + release_id: str, |
| 302 | + expected_current_release_id: str, |
| 303 | + expected_generation: int, |
| 304 | + audit_id: str, |
| 305 | + occurred_at: datetime, |
| 306 | +) -> dict[str, Any]: |
| 307 | + manifest_snapshot = manifest_ref.get(transaction=transaction) |
| 308 | + if not getattr(manifest_snapshot, "exists", False): |
| 309 | + raise ValueError("rollback target release manifest does not exist") |
| 310 | + raw_manifest: object = manifest_snapshot.to_dict() |
| 311 | + manifest_data = cast(dict[str, Any], raw_manifest) if isinstance(raw_manifest, dict) else {} |
| 312 | + manifest = normalize_release_manifest(manifest_data) |
| 313 | + |
| 314 | + pointer_snapshot = pointer_ref.get(transaction=transaction) |
| 315 | + if not getattr(pointer_snapshot, "exists", False): |
| 316 | + raise ValueError("current macos beta pointer does not exist") |
| 317 | + current_raw: object = pointer_snapshot.to_dict() |
| 318 | + current = cast(dict[str, Any], current_raw) if isinstance(current_raw, dict) else {} |
| 319 | + pointer = _build_beta_rollback_pointer( |
| 320 | + current, |
| 321 | + manifest, |
| 322 | + release_id=release_id, |
| 323 | + expected_current_release_id=expected_current_release_id, |
| 324 | + expected_generation=expected_generation, |
| 325 | + updated_at=occurred_at, |
| 326 | + ) |
| 327 | + audit = { |
| 328 | + "audit_id": audit_id, |
| 329 | + "operation": "macos_beta_rollback", |
| 330 | + "platform": "macos", |
| 331 | + "channel": "beta", |
| 332 | + "previous_release_id": expected_current_release_id, |
| 333 | + "previous_generation": expected_generation, |
| 334 | + "target_release_id": release_id, |
| 335 | + "generation": pointer["generation"], |
| 336 | + "occurred_at": occurred_at, |
| 337 | + } |
| 338 | + # create() provides an immutable, append-only audit record. All reads above |
| 339 | + # occur before this first transactional write. |
| 340 | + transaction.create(audit_ref, audit) |
| 341 | + transaction.set(pointer_ref, pointer) |
| 342 | + return {"pointer": pointer, "audit": audit} |
| 343 | + |
| 344 | + |
| 345 | +def rollback_macos_beta_channel( |
| 346 | + release_id: str, |
| 347 | + *, |
| 348 | + expected_current_release_id: str, |
| 349 | + expected_generation: int, |
| 350 | + firestore_client: Any = None, |
| 351 | +) -> dict[str, Any]: |
| 352 | + """Atomically roll macOS beta back to an earlier, qualified registered release only.""" |
| 353 | + release_id = release_id.strip() |
| 354 | + expected_current_release_id = expected_current_release_id.strip() |
| 355 | + if not release_id: |
| 356 | + raise ValueError("release_id is required") |
| 357 | + if not expected_current_release_id: |
| 358 | + raise ValueError("expected_current_release_id is required") |
| 359 | + if expected_generation < 0: |
| 360 | + raise ValueError("expected_generation must be a non-negative integer") |
| 361 | + |
| 362 | + client = firestore_client if firestore_client is not None else get_firestore_client() |
| 363 | + pointer_ref = client.collection(CHANNELS_COLLECTION).document("macos-beta") |
| 364 | + manifest_ref = client.collection(MANIFESTS_COLLECTION).document(release_id) |
| 365 | + audit_id = uuid4().hex |
| 366 | + audit_ref = client.collection(ROLLBACK_AUDITS_COLLECTION).document(audit_id) |
| 367 | + return _rollback_macos_beta_transaction( |
| 368 | + client.transaction(), |
| 369 | + pointer_ref, |
| 370 | + manifest_ref, |
| 371 | + audit_ref, |
| 372 | + release_id=release_id, |
| 373 | + expected_current_release_id=expected_current_release_id, |
| 374 | + expected_generation=expected_generation, |
| 375 | + audit_id=audit_id, |
| 376 | + occurred_at=datetime.now(timezone.utc), |
| 377 | + ) |
| 378 | + |
| 379 | + |
252 | 380 | def get_channel_release(platform: str, channel: str, *, firestore_client: Any = None) -> dict[str, Any] | None: |
253 | 381 | """Resolve one explicit channel pointer to its immutable manifest.""" |
254 | 382 | if platform not in VALID_PLATFORMS or channel not in VALID_CHANNELS: |
|
0 commit comments