From 5956716faea1784a7dd27d334ed6903c2091305b Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:20:39 +0800 Subject: [PATCH 1/3] fix: isolate candidate roots without moving notification markers Co-Authored-By: Codex --- .../workflows/execution-report-heartbeat.yml | 1 + docs/unified-daily-notification.md | 18 +++++++++ notifications/unified.py | 16 +++++++- tests/test_notification_scoped_state.py | 37 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/.github/workflows/execution-report-heartbeat.yml b/.github/workflows/execution-report-heartbeat.yml index 35d589a..5f3d748 100644 --- a/.github/workflows/execution-report-heartbeat.yml +++ b/.github/workflows/execution-report-heartbeat.yml @@ -102,6 +102,7 @@ jobs: TELEGRAM_TOKEN_SECRET_NAME: ${{ vars.TELEGRAM_TOKEN_SECRET_NAME }} FIRSTRADE_UNIFIED_NOTIFICATION_ENABLED: ${{ vars.FIRSTRADE_UNIFIED_NOTIFICATION_ENABLED || 'false' }} DIGEST_CANDIDATES_GCS_URI: ${{ vars.DIGEST_CANDIDATES_GCS_URI || secrets.DIGEST_CANDIDATES_GCS_URI }} + DIGEST_CANDIDATES_OVERRIDE_GCS_URI: ${{ vars.DIGEST_CANDIDATES_OVERRIDE_GCS_URI }} DIGEST_STORE_TARGET: ${{ vars.DIGEST_STORE_TARGET }} DIGEST_OPAQUE_ACCOUNT_UID: ${{ secrets.DIGEST_OPAQUE_ACCOUNT_UID }} DIGEST_TARGET_ID: ${{ secrets.DIGEST_TARGET_ID }} diff --git a/docs/unified-daily-notification.md b/docs/unified-daily-notification.md index 484805e..a407319 100644 --- a/docs/unified-daily-notification.md +++ b/docs/unified-daily-notification.md @@ -38,6 +38,24 @@ and receipts have been migrated; changing the prefix alone would discard the history used to prevent duplicate alerts. Per-target bindings may set the variable independently. +For a candidate-only bucket migration in the heartbeat workflow, set the +protected repository variable `DIGEST_CANDIDATES_OVERRIDE_GCS_URI`. It overrides +the candidate root after each private target binding is resolved, so an older +`DIGEST_TARGET_BINDINGS_JSON` cannot silently select the previous bucket. The +existing explicit marker URI is preserved; when absent, the binding's original +candidate root remains its marker root. Target identity, archive source and +enablement are unchanged. Missing or invalid candidate access never falls back +to the previous bucket. An unset override preserves existing behavior. + +The standalone single-target publisher and runtime can instead update their +explicit `DIGEST_CANDIDATES_GCS_URI` after freezing the existing marker location +in `QSL_NOTIFICATION_MARKER_GCS_URI`. Pause all candidate writers outside the +publication window, copy and verify the selected candidate objects, switch the +writers, and then validate the central source through its no-send preview. +Keep the selected new root during application rollback and retain every marker +and central delivery reservation. Remove superseded permissions only after at +least two successful natural digest windows. + No deployment, variable application, IAM, secret or live message is authorized by this PR. Source adoption and delivery require separate approved release work. diff --git a/notifications/unified.py b/notifications/unified.py index 0652bfd..523831f 100644 --- a/notifications/unified.py +++ b/notifications/unified.py @@ -11,6 +11,7 @@ import hashlib import json import os +import re from notifications.digest_snapshot import digest_archive from notifications.digest_snapshot.digest_candidates import project_reports @@ -115,7 +116,20 @@ def _bindings(env): values = json.loads(raw) if raw else [{}] if not isinstance(values, list) or not values or not all(isinstance(v, dict) for v in values): raise ValueError("candidate_bindings_invalid") - return [{**env, **v, "DIGEST_TARGET_BINDINGS_JSON": ""} for v in values] + bindings = [{**env, **v, "DIGEST_TARGET_BINDINGS_JSON": ""} for v in values] + override = env.get("DIGEST_CANDIDATES_OVERRIDE_GCS_URI", "") + if override: + if (not isinstance(override, str) + or not re.fullmatch(r"gs://[a-z0-9][a-z0-9._-]+/[^\s?#*]+", override) + or any(part in {"", ".", ".."} for part in override[5:].rstrip("/").split("/")[1:])): + raise ValueError("candidate_store_configuration_missing") + for binding in bindings: + marker = binding.get("QSL_NOTIFICATION_MARKER_GCS_URI") or binding.get("DIGEST_CANDIDATES_GCS_URI") + if not marker: + raise ValueError("candidate_store_configuration_missing") + binding["QSL_NOTIFICATION_MARKER_GCS_URI"] = marker + binding["DIGEST_CANDIDATES_GCS_URI"] = override.rstrip("/") + return bindings def _binding(env, report): diff --git a/tests/test_notification_scoped_state.py b/tests/test_notification_scoped_state.py index efdf29d..c942d43 100644 --- a/tests/test_notification_scoped_state.py +++ b/tests/test_notification_scoped_state.py @@ -1,11 +1,48 @@ """Scoped notification state keeps the shared candidate writer narrowly bound.""" import re +import json + +import pytest from notifications import unified from test_notification_consolidation import ENV, NOW, Store, notify, source STATE = "gs://synthetic/notification-markers/firstrade/synthetic-account" +ISOLATED = "gs://synthetic-isolated/candidates" + + +def test_candidate_override_wins_over_private_binding_and_preserves_marker_default(): + old = "gs://synthetic-bound/candidates" + env = {**ENV, "DIGEST_CANDIDATES_OVERRIDE_GCS_URI": ISOLATED, + "DIGEST_TARGET_BINDINGS_JSON": json.dumps([{"DIGEST_CANDIDATES_GCS_URI": old}])} + binding, = unified._bindings(env) + assert binding["DIGEST_CANDIDATES_GCS_URI"] == ISOLATED + assert unified._state_prefix(binding) == old + assert unified._bindings(binding) == [binding] + assert json.loads(env["DIGEST_TARGET_BINDINGS_JSON"])[0]["DIGEST_CANDIDATES_GCS_URI"] == old + + +def test_candidate_override_preserves_existing_incident_deduplication(): + store, sent = Store(), [] + report, result = source(status="blocked", execution_blocked=True) + env = {**ENV, "QSL_NOTIFICATION_MARKER_GCS_URI": STATE} + notify(store, report, result, sent, env=env) + migrated = {**env, "DIGEST_CANDIDATES_OVERRIDE_GCS_URI": ISOLATED} + repeated = notify(store, report, result, sent, env=migrated) + assert repeated["delivery_status"] == "duplicate" + assert len(sent) == 1 + assert any(key.startswith(STATE + "/_notification_markers/") for key in store.data) + binding, = unified._bindings(migrated) + assert binding["DIGEST_CANDIDATES_GCS_URI"] == ISOLATED + assert unified._state_prefix(binding) == STATE + + +@pytest.mark.parametrize("uri", ["https://synthetic/path", "gs://synthetic", "gs://synthetic/a/../b", + "gs://synthetic/a?private=value", "gs://synthetic/a/*"]) +def test_invalid_candidate_override_fails_before_storage(uri): + with pytest.raises(ValueError, match="^candidate_store_configuration_missing$"): + unified._bindings({**ENV, "DIGEST_CANDIDATES_OVERRIDE_GCS_URI": uri}) class ScopedStore(Store): From 0a45a27821101957cf8f257383d19a0faa6c2ba0 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:21:29 +0800 Subject: [PATCH 2/3] fix: require a nonempty candidate override prefix Co-Authored-By: Codex --- notifications/unified.py | 4 +++- tests/test_notification_scoped_state.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/notifications/unified.py b/notifications/unified.py index 523831f..28af849 100644 --- a/notifications/unified.py +++ b/notifications/unified.py @@ -119,6 +119,7 @@ def _bindings(env): bindings = [{**env, **v, "DIGEST_TARGET_BINDINGS_JSON": ""} for v in values] override = env.get("DIGEST_CANDIDATES_OVERRIDE_GCS_URI", "") if override: + override = override.rstrip("/") if isinstance(override, str) else override if (not isinstance(override, str) or not re.fullmatch(r"gs://[a-z0-9][a-z0-9._-]+/[^\s?#*]+", override) or any(part in {"", ".", ".."} for part in override[5:].rstrip("/").split("/")[1:])): @@ -128,7 +129,8 @@ def _bindings(env): if not marker: raise ValueError("candidate_store_configuration_missing") binding["QSL_NOTIFICATION_MARKER_GCS_URI"] = marker - binding["DIGEST_CANDIDATES_GCS_URI"] = override.rstrip("/") + binding["DIGEST_CANDIDATES_GCS_URI"] = override + binding["DIGEST_CANDIDATES_OVERRIDE_GCS_URI"] = override return bindings diff --git a/tests/test_notification_scoped_state.py b/tests/test_notification_scoped_state.py index c942d43..34ae76c 100644 --- a/tests/test_notification_scoped_state.py +++ b/tests/test_notification_scoped_state.py @@ -38,7 +38,7 @@ def test_candidate_override_preserves_existing_incident_deduplication(): assert unified._state_prefix(binding) == STATE -@pytest.mark.parametrize("uri", ["https://synthetic/path", "gs://synthetic", "gs://synthetic/a/../b", +@pytest.mark.parametrize("uri", ["https://synthetic/path", "gs://synthetic", "gs://synthetic///", "gs://synthetic/a/../b", "gs://synthetic/a?private=value", "gs://synthetic/a/*"]) def test_invalid_candidate_override_fails_before_storage(uri): with pytest.raises(ValueError, match="^candidate_store_configuration_missing$"): From ad92090bdd9a8a9dd3f10c8a5b66495dfb723fc8 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:23:13 +0800 Subject: [PATCH 3/3] test: verify isolated candidate creation and same-day updates Co-Authored-By: Codex --- tests/test_notification_scoped_state.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_notification_scoped_state.py b/tests/test_notification_scoped_state.py index 34ae76c..883d446 100644 --- a/tests/test_notification_scoped_state.py +++ b/tests/test_notification_scoped_state.py @@ -38,6 +38,22 @@ def test_candidate_override_preserves_existing_incident_deduplication(): assert unified._state_prefix(binding) == STATE +def test_candidate_override_creates_and_updates_only_the_selected_root(): + store, sent = Store(), [] + env = {**ENV, "DIGEST_CANDIDATES_OVERRIDE_GCS_URI": ISOLATED} + for run_id in ("first-run", "second-run"): + report, result = source(run_id) + assert notify(store, report, result, sent, env=env)["route"] == "digest_candidate" + candidates = [(key, value) for key, value in store.data.items() + if value[1].get("schema_version") == "qsl.digest_candidates.v1"] + assert len(candidates) == 1 + key, (generation, body) = candidates[0] + assert key.startswith(ISOLATED + "/firstrade/") + assert generation == 2 + assert {row["source_run_id"] for row in body["runs"]} == {"first-run", "second-run"} + assert not sent + + @pytest.mark.parametrize("uri", ["https://synthetic/path", "gs://synthetic", "gs://synthetic///", "gs://synthetic/a/../b", "gs://synthetic/a?private=value", "gs://synthetic/a/*"]) def test_invalid_candidate_override_fails_before_storage(uri):