Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bc645ee
feat: add queued sync status and trigger syncs for referencing enviro…
rohan-chaturvedi Mar 25, 2026
6809c82
feat: refactor sync dispatch, add queued lifecycle, and trigger syncs…
rohan-chaturvedi Mar 25, 2026
d430de7
feat: add env_has_references_to for detecting cross-env and cross-app…
rohan-chaturvedi Mar 25, 2026
42bf984
feat: add migration for queued sync status and default changes
rohan-chaturvedi Mar 25, 2026
4a12726
test: add tests for env_has_references_to and trigger_syncs_for_refer…
rohan-chaturvedi Mar 25, 2026
4f05027
chore: regenerate GraphQL schema and types with queued sync status
rohan-chaturvedi Mar 25, 2026
6b4240d
feat: handle queued sync status in frontend sync components
rohan-chaturvedi Mar 25, 2026
28e4bee
feat: preserve env ordering in sync status menu
rohan-chaturvedi Mar 25, 2026
aed542b
fix: type
rohan-chaturvedi Mar 25, 2026
4e71726
Merge branch 'main' into feat--syncing-with-references
rohan-chaturvedi Mar 27, 2026
bb513c1
Merge branch 'main' into feat--syncing-with-references
rohan-chaturvedi May 8, 2026
a8fe34a
Merge branch 'main' into feat--syncing-with-references
nimish-ks May 11, 2026
15520f9
Merge branch 'main' into feat--syncing-with-references
nimish-ks May 23, 2026
8e319c2
Merge branch 'main' into feat--syncing-with-references
rohan-chaturvedi May 26, 2026
1868263
Merge branch 'main' into feat--syncing-with-references
rohan-chaturvedi Jun 20, 2026
edc9fa1
fix(syncing): recursive nested reference resolution and once-per-bulk…
rohan-chaturvedi Jun 21, 2026
ceb3166
Merge branch 'main' into feat--syncing-with-references
nimish-ks Jun 22, 2026
4bfefc8
fix(secrets): record override-less bulk PUT updates and access-check …
rohan-chaturvedi Jun 23, 2026
20f2844
Merge branch 'main' into feat--syncing-with-references
nimish-ks Jun 23, 2026
8211d98
perf(syncing): run cross-reference sync detection off the request path
rohan-chaturvedi Jun 24, 2026
f22c5cf
Merge remote-tracking branch 'origin/main' into feat--syncing-with-re…
rohan-chaturvedi Jun 24, 2026
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
10 changes: 4 additions & 6 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from django.utils import timezone
from django.conf import settings
from api.services import Providers, ServiceConfig
from api.tasks.syncing import trigger_sync_tasks, trigger_syncs_for_referencing_envs
from api.tasks.syncing import trigger_sync_tasks, detect_and_trigger_referencing_syncs
from backend.quotas import (
can_add_account,
can_add_app,
Expand Down Expand Up @@ -467,10 +467,9 @@ class Environment(models.Model):
objects = EnvironmentManager()

def save(self, *args, **kwargs):
# Call the "real" save() method to save the Secret
super().save(*args, **kwargs)

# Trigger all sync jobs associated with this environment
# Own syncs: synchronous, so queued status shows immediately.
[
trigger_sync_tasks(env_sync)
for env_sync in EnvironmentSync.objects.filter(
Expand All @@ -479,9 +478,8 @@ def save(self, *args, **kwargs):
if env_sync.is_active
]

# Trigger syncs for other environments whose secrets
# reference this one (cross-env and cross-app references)
trigger_syncs_for_referencing_envs(self)
# Referencing envs: deferred to a worker (detection decrypts + walks the org graph).
detect_and_trigger_referencing_syncs.delay(str(self.id))


class EnvironmentKey(models.Model):
Expand Down
28 changes: 26 additions & 2 deletions backend/api/tasks/syncing.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,11 @@ def trigger_syncs_for_referencing_envs(changed_env):
this we build the org's reference graph and follow it from each candidate
environment until we reach the changed environment (cycle-safe).

Called synchronously from Environment.save() so the pending sync status is
reflected in the UI right away; the actual sync work is dispatched async.
Runs off the request path, dispatched from Environment.save() via the
detect_and_trigger_referencing_syncs RQ job — so referencing envs' syncs are
queued shortly after the write, not within it. (The changed environment's own
syncs are still triggered synchronously in Environment.save for immediate
status.) The actual provider sync work is dispatched async by trigger_sync_tasks.
"""
from api.utils.secrets import get_referenced_environment_ids

Expand Down Expand Up @@ -602,3 +605,24 @@ def references_changed_env(start_env_id):
logger.warning(
f"Failed to check references for environment {env_id}: {e}"
)


@job("default", timeout=DEFAULT_TIMEOUT)
def detect_and_trigger_referencing_syncs(changed_env_id):
"""
Async entrypoint for trigger_syncs_for_referencing_envs.

Dispatched from Environment.save() so the reference-graph detection — which
decrypts and scans every synced/intermediate environment in the org — runs
off the request path instead of blocking the secret write. The changed
environment's own syncs are still triggered synchronously by Environment.save
for immediate status feedback; only the cross-reference fan-out is deferred.
"""
Environment = apps.get_model("api", "Environment")
try:
changed_env = Environment.objects.select_related(
"app", "app__organisation"
).get(id=changed_env_id)
except Environment.DoesNotExist:
return
trigger_syncs_for_referencing_envs(changed_env)
39 changes: 38 additions & 1 deletion backend/tests/tasks/test_syncing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,42 @@
from unittest.mock import patch, MagicMock
from api.tasks.syncing import trigger_syncs_for_referencing_envs
from api.tasks.syncing import (
trigger_syncs_for_referencing_envs,
detect_and_trigger_referencing_syncs,
)


# --- detect_and_trigger_referencing_syncs (async wrapper) tests ---


@patch("api.tasks.syncing.trigger_syncs_for_referencing_envs")
@patch("api.tasks.syncing.apps.get_model")
def test_detect_and_trigger_refetches_env_and_delegates(mock_get_model, mock_trigger):
"""The async job re-fetches the env by id and runs the detection."""
env = MagicMock()
MockEnvironment = MagicMock()
MockEnvironment.objects.select_related.return_value.get.return_value = env
MockEnvironment.DoesNotExist = type("DoesNotExist", (Exception,), {})
mock_get_model.return_value = MockEnvironment

detect_and_trigger_referencing_syncs("env-id")

mock_trigger.assert_called_once_with(env)


@patch("api.tasks.syncing.trigger_syncs_for_referencing_envs")
@patch("api.tasks.syncing.apps.get_model")
def test_detect_and_trigger_missing_env_is_noop(mock_get_model, mock_trigger):
"""If the env was deleted before the job runs, it returns without error."""
MockEnvironment = MagicMock()
MockEnvironment.DoesNotExist = type("DoesNotExist", (Exception,), {})
MockEnvironment.objects.select_related.return_value.get.side_effect = (
MockEnvironment.DoesNotExist()
)
mock_get_model.return_value = MockEnvironment

detect_and_trigger_referencing_syncs("missing-env")

mock_trigger.assert_not_called()


# --- trigger_syncs_for_referencing_envs tests ---
Expand Down
Loading