feat: sync envs with references#827
Merged
Merged
Conversation
… for cross-references
… secret references
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: Rohan Chaturvedi <rohan.chaturvedi@protonmail.com>
Signed-off-by: Rohan Chaturvedi <rohan.chaturvedi@protonmail.com>
…override references
nimish-ks
approved these changes
Jun 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🔍 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:
${L2}→${L1}→${L0}) were pushed to providers partially unresolved (#877).${LOCAL}-${env.KEY}) mis-parsed — the reference regex spanned across the local ref, producing a hard sync failure and a garbled editor highlight.💡 Proposed Changes
Recursive reference resolution (Closes #877)
resolve_secret_value()now resolves a referenced secret throughdecrypt_secret_value(), so references nested inside a referenced value (local, cross-env, cross-app) are themselves resolved — recursively, to any depth._visitedcycle detection keyed by(env, path, key_digest)+ aMAX_REFERENCE_DEPTHcap.account,require_resolved_references, andcontext_cacheare threaded through every hop, so permission checks and crypto-context caching apply at each level.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, andLOCAL_REF_PATTERNnow 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
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 earlierenv_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 inCre-triggersB).Queuedsync statusQUEUEDtoEnvironmentSync/EnvironmentSyncEvent; default status is nowQUEUED.trigger_sync_tasks()setsQUEUEDat dispatch →handle_sync_event()flips toIN_PROGRESSwhen a worker picks the job up.cancel_sync_tasks()cancels bothQUEUEDandIN_PROGRESS.Batched sync triggering for bulk writes
Secret.save(trigger_sync=True)/Secret.delete(trigger_sync=True)— bulk callers passtrigger_sync=Falseand trigger once per affected environment after the loop, instead of once per secret. Applied to GraphQLBulkCreate/Edit/Deleteand the RESTE2EE/PublicPOST/PUT/DELETEendpoints. Single-secret paths are unchanged (defaultTrue).Refactor / robustness
trigger_sync_tasks()collapsed from 12 duplicatedif/elifbranches into a service dispatch map, withtry/exceptaround.delay()so a dispatch failure marks the syncFAILEDinstead of leaving it stale.Frontend
Queuedrendering inSyncStatusIndicator(clock icon + label);EnvSyncStatustreatsQueuedas in-progress;SyncCard,SyncHistory, andSyncManagementsuppress the "last sync" timestamp / show a spinner / disable "Sync now" while queued.environment.index).secretReferences.tsreference-pattern fix (highlighting / validation / autocomplete no longer mis-parse combined refs — fixes the doubled, unselectable highlight overlay).Schema & migration
0131_add_queued_sync_status(regenerated to apply cleanly on the current migration graph).ApiSecretEventTypeChoicesenum from the generated schema; regenerated schema/types with theQUEUEDvalue.🖼️ 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
Queuedsync status so the UI accurately shows a sync waiting for a worker before it'sIn Progress.${LOCAL}-${env.KEY}failed to resolve (and rendered a doubled artifact in the editor).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):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) —
secretReferences88 passed, including new combined-reference parsing tests and an overlay "no doubling" regression.tsc --noEmitclean.Manual / dogfooding (live instance, public REST API):
GET /public/v1/secrets/.get_environment_secrets) and observed thequeued → in_progress → completedlifecycle 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:
backend/api/utils/secrets.py—decrypt_secret_value()/resolve_secret_value()recursion + cycle/depth guards, the three reference regexes, andget_referenced_environment_ids().backend/api/tasks/syncing.py—trigger_syncs_for_referencing_envs()graph walk, and the queued lifecycle intrigger_sync_tasks()/handle_sync_event()/cancel_sync_tasks().backend/api/models.pySecret.save(trigger_sync=...)+ the bulk call sites inbackend/backend/graphene/mutations/environment.pyandbackend/api/views/secrets.py(confirm single paths still trigger; bulk paths trigger once).frontend/utils/secretReferences.ts— the regex change.➕ Additional Context
${KEY}/${path/KEY}, cross-env${ENV.KEY}, cross-app${APP::ENV.KEY}— all resolve recursively server-side.✨ How to Test the Changes Locally
docker compose -f dev-docker-compose.yml up -d) and ensure SSE is enabled on the test app.L0=value,L1=${L0}-x,L2=${L1}-y. Read it back via the UI orGET /public/v1/secrets/and confirmL2resolves tovalue-x-y(not${L0}-x-y).${L2}|${Prod.DB}|${App::Prod.KEY}; confirm all three segments resolve.${A}-${Production.DEBUG}and confirm it highlights as two distinct references (no doubled artifact).💚 Did You...
tsc --noEmitclean; backendmanage.py checkclean — run your formatter to be sure)0131_add_queued_sync_status)