Skip to content

feat: sync envs with references#827

Merged
nimish-ks merged 19 commits into
mainfrom
feat--syncing-with-references
Jun 24, 2026
Merged

feat: sync envs with references#827
nimish-ks merged 19 commits into
mainfrom
feat--syncing-with-references

Conversation

@rohan-chaturvedi

@rohan-chaturvedi rohan-chaturvedi commented Mar 25, 2026

Copy link
Copy Markdown
Member

🔍 Overview

This PR extends the syncing engine to understand secret references — so that when a referenced secret changes, the syncs that depend on it are kept up to date, and so that the values pushed to third-party providers are fully resolved.

While building this out, other issues in the existing reference-resolution path surfaced and are fixed here:

  • Server-side reference resolution only expanded one level deep, so chained references (${L2}${L1}${L0}) were pushed to providers partially unresolved (#877).
  • A value mixing a dot-less local ref before a dotted ref (${LOCAL}-${env.KEY}) mis-parsed — the reference regex spanned across the local ref, producing a hard sync failure and a garbled editor highlight.
  • Bulk secret writes triggered the (now heavier) reference scan once per secret, synchronously, on the request path.

💡 Proposed Changes

Recursive reference resolution (Closes #877)

  • resolve_secret_value() now resolves a referenced secret through decrypt_secret_value(), so references nested inside a referenced value (local, cross-env, cross-app) are themselves resolved — recursively, to any depth.
  • Terminates safely: per-branch _visited cycle detection keyed by (env, path, key_digest) + a MAX_REFERENCE_DEPTH cap. account, require_resolved_references, and context_cache are threaded through every hop, so permission checks and crypto-context caching apply at each level.
  • This is the same engine used by both third-party syncs (get_environment_secrets) and the public REST API (SecretSerializer), so the fix lands in both.

Reference-pattern fix (combined references)

  • CROSS_APP_ENV_PATTERN, CROSS_ENV_PATTERN, and LOCAL_REF_PATTERN now exclude {/} from their capture groups, so a single reference can no longer span across an adjacent ${...}. Mirrored in the frontend (secretReferences.ts).

Trigger syncs for referencing environments — transitively

  • Added get_referenced_environment_ids() (api/utils/secrets.py) — returns the set of environment IDs an environment's secrets reference (names resolved to IDs). Replaces the earlier env_has_references_to.
  • trigger_syncs_for_referencing_envs() (api/tasks/syncing.py) builds the org's reference graph and walks it (cycle-safe, decryption memoized) so a change re-triggers syncs that depend on it directly or transitively (e.g. B → A → C: a change in C re-triggers B).

Queued sync status

  • Added QUEUED to EnvironmentSync / EnvironmentSyncEvent; default status is now QUEUED.
  • Lifecycle: trigger_sync_tasks() sets QUEUED at dispatch → handle_sync_event() flips to IN_PROGRESS when a worker picks the job up. cancel_sync_tasks() cancels both QUEUED and IN_PROGRESS.

Batched sync triggering for bulk writes

  • Secret.save(trigger_sync=True) / Secret.delete(trigger_sync=True) — bulk callers pass trigger_sync=False and trigger once per affected environment after the loop, instead of once per secret. Applied to GraphQL BulkCreate/Edit/Delete and the REST E2EE/Public POST/PUT/DELETE endpoints. Single-secret paths are unchanged (default True).

Refactor / robustness

  • trigger_sync_tasks() collapsed from 12 duplicated if/elif branches into a service dispatch map, with try/except around .delay() so a dispatch failure marks the sync FAILED instead of leaving it stale.

Frontend

  • Queued rendering in SyncStatusIndicator (clock icon + label); EnvSyncStatus treats Queued as in-progress; SyncCard, SyncHistory, and SyncManagement suppress the "last sync" timestamp / show a spinner / disable "Sync now" while queued.
  • Env ordering preserved in the sync-status menu (sort by environment.index).
  • secretReferences.ts reference-pattern fix (highlighting / validation / autocomplete no longer mis-parse combined refs — fixes the doubled, unselectable highlight overlay).

Schema & migration

  • Migration 0131_add_queued_sync_status (regenerated to apply cleanly on the current migration graph).
  • Removed a duplicate ApiSecretEventTypeChoices enum from the generated schema; regenerated schema/types with the QUEUED value.

🖼️ Screenshots or Demo

Screencast.From.2026-03-25.17-48-14.mp4

Resolution and the queued lifecycle were also dogfooded against a live instance via the public REST API (see Testing).

📝 Release Notes

  • Chained secret references now fully resolve when secrets are synced to third-party providers and when read via the REST API (previously only one level deep). Fixes Secret sync for Render doesn't support secret referencing for more than 1 level #877.
  • Syncs stay in step with referenced secrets: changing a secret now re-runs syncs for any environment that references it, directly or through a chain of references.
  • New Queued sync status so the UI accurately shows a sync waiting for a worker before it's In Progress.
  • Fixed a parsing bug where a reference like ${LOCAL}-${env.KEY} failed to resolve (and rendered a doubled artifact in the editor).
  • Performance: bulk secret writes now trigger dependent syncs once per environment instead of once per secret.
  • Includes a database migration (0131_add_queued_sync_status). No breaking changes — existing single-secret behavior is preserved.

🧪 Testing

Backend (pytest)1052 passed (1 unrelated pre-existing failure: test_file_read_permission_error, which fails only because tests run as root in-container):

  • Recursive resolution: multi-level local chain, nested cross-env, and reference-cycle safety.
  • Reference patterns don't span adjacent references (cross-env / cross-app / combined).
  • get_referenced_environment_ids (cross-env, cross-app, ambiguous-app skip, no-SSE, Railway syntax).
  • trigger_syncs_for_referencing_envs (direct, transitive, cycle-safe, none, multi-env).

Frontend (jest)secretReferences 88 passed, including new combined-reference parsing tests and an overlay "no doubling" regression. tsc --noEmit clean.

Manual / dogfooding (live instance, public REST API):

  • Created direct, folder, cross-env, cross-env-folder, cross-app (recursive), cross-app→cross-env, and combined references across multiple apps; verified every type resolves correctly over GET /public/v1/secrets/.
  • Examined the resolved sync payload (get_environment_secrets) and observed the queued → in_progress → completed lifecycle on real GitHub Actions / Dependabot syncs.

Gaps: no end-to-end automated test exercises a full bulk-mutation → single-trigger flow against a DB (covered manually); transitive-trigger tests stub get_referenced_environment_ids (the resolver itself is covered separately).

🎯 Reviewer Focus

Suggested entry points:

  1. backend/api/utils/secrets.pydecrypt_secret_value() / resolve_secret_value() recursion + cycle/depth guards, the three reference regexes, and get_referenced_environment_ids().
  2. backend/api/tasks/syncing.pytrigger_syncs_for_referencing_envs() graph walk, and the queued lifecycle in trigger_sync_tasks() / handle_sync_event() / cancel_sync_tasks().
  3. backend/api/models.py Secret.save(trigger_sync=...) + the bulk call sites in backend/backend/graphene/mutations/environment.py and backend/api/views/secrets.py (confirm single paths still trigger; bulk paths trigger once).
  4. frontend/utils/secretReferences.ts — the regex change.

➕ Additional Context

✨ How to Test the Changes Locally

  1. Bring up the dev stack (docker compose -f dev-docker-compose.yml up -d) and ensure SSE is enabled on the test app.
  2. Create a chain in one environment: L0=value, L1=${L0}-x, L2=${L1}-y. Read it back via the UI or GET /public/v1/secrets/ and confirm L2 resolves to value-x-y (not ${L0}-x-y).
  3. Add cross-env / cross-app references and a combined value like ${L2}|${Prod.DB}|${App::Prod.KEY}; confirm all three segments resolve.
  4. Configure a sync on an environment that references another; change a secret in the referenced environment and confirm the dependent sync is (re)triggered, briefly showing Queued before In Progress.
  5. In the secret editor, type ${A}-${Production.DEBUG} and confirm it highlights as two distinct references (no doubled artifact).

💚 Did You...

  • Ensure linting passes (code style checks)? (frontend tsc --noEmit clean; backend manage.py check clean — run your formatter to be sure)
  • Update dependencies and lockfiles (if required) (N/A — no new dependencies)
  • Update migrations (if required) (0131_add_queued_sync_status)
  • Regenerate graphql schema and types (if required) (also removed a duplicate enum)
  • Verify the app builds locally?
  • Manually test the changes on different browsers/devices? (dogfooded via REST + UI editor)

@rohan-chaturvedi rohan-chaturvedi added enhancement New feature or request frontend Change in frontend code backend updates migrations This PR adds new migrations that update the database schema labels Mar 25, 2026
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: Rohan Chaturvedi <rohan.chaturvedi@protonmail.com>
@nimish-ks nimish-ks merged commit 081cadf into main Jun 24, 2026
15 checks passed
@nimish-ks nimish-ks deleted the feat--syncing-with-references branch June 24, 2026 09:55
@rohan-chaturvedi rohan-chaturvedi restored the feat--syncing-with-references branch June 24, 2026 10:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend enhancement New feature or request frontend Change in frontend code updates migrations This PR adds new migrations that update the database schema

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Secret sync for Render doesn't support secret referencing for more than 1 level

2 participants