feat(secret-sync): add Render workflow scope to secret sync#6906
feat(secret-sync): add Render workflow scope to secret sync#6906jaydeep-pipaliya wants to merge 3 commits into
Conversation
Render workflows are a separate API resource at /v1/workflows and
cannot be listed via /v1/services regardless of query parameters.
Workflow env vars are also not managed through the /env-vars sub-path
used by services — they are read from GET /v1/workflows/{id} and
written via PATCH /v1/workflows/{id} with a full envVars replacement.
This adds:
- A new `workflow` scope to RenderSyncScope
- `listRenderWorkflows` that pages through GET /v1/workflows
- A GET /:connectionId/workflows connection endpoint to feed the UI selector
- Workflow cases in all sync operations (get, sync, remove)
- Auto-redeploy is a no-op for workflows since they have no deploy endpoint
Fixes Infisical#6409
|
| Filename | Overview |
|---|---|
| backend/src/services/secret-sync/render/render-sync-fns.ts | Core sync logic extended for Workflow scope; PATCH-based full replacement is correct, but unsanitized workflowId in URL paths creates a path-traversal risk, and the Step 4 comment is misleading. |
| backend/src/services/app-connection/render/render-connection-fns.ts | New listRenderWorkflows closely mirrors existing pagination logic for services/env-groups; minor: no guard against missing workflow key in API response items. |
| backend/src/services/secret-sync/render/render-sync-schemas.ts | New Workflow discriminated-union variant added correctly; type field is included but unused in all workflow code paths. |
| backend/src/server/routes/v1/app-connection-routers/render-connection-router.ts | New GET /:connectionId/workflows route follows existing pattern exactly — UUID-validated param, JWT auth, rate-limit, proper response schema. |
| backend/src/services/app-connection/render/render-connection-service.ts | Adds listWorkflows method mirroring existing listServices and listEnvironmentGroups patterns with identical error handling. |
| backend/src/services/app-connection/render/render-connection-types.ts | Three new types (TRenderWorkflow, TRawRenderWorkflow, TRenderWorkflowDetail) added correctly; shapes match Render API response structure. |
| backend/src/services/secret-sync/render/render-sync-enums.ts | Adds Workflow = "workflow" to RenderSyncScope enum — straightforward and consistent. |
| backend/src/lib/api-docs/constants.ts | Adds workflowId documentation constant under RENDER destination config — minimal, correct change. |
Comments Outside Diff (1)
-
backend/src/services/secret-sync/render/render-sync-fns.ts, line 356-358 (link)Workflow deletions silently skipped when
disableSecretDeletionis enabledThe
syncSecretsStep 4 guard only invokes the explicit per-key delete forEnvironmentGroupscope. ForWorkflowscope this is intentional because the PATCH is a full replacement — but whendisableSecretDeletionistrue, the logic in Step 1 preserves the secret infinalEnvVars, so the PATCH correctly keeps it. However whendisableSecretDeletionisfalse, secrets that were removed from Infisical go intosecretsToDelete(notfinalEnvVars), and since the PATCH only sendsfinalEnvVars, those secrets are effectively deleted even though the comment says they follow the standard path. The behavior is actually correct, but the comment on Step 4 ("only for environment groups because it doesn't delete on the update call") is misleading — it implies Service is handled differently, but the same full-replacement logic applies to Workflow. Adding a brief inline note clarifying that Workflow deletions are implicit in the PATCH would help future maintainers.
Reviews (1): Last reviewed commit: "feat(secret-sync): add Render workflow s..." | Re-trigger Greptile
| case RenderSyncScope.Workflow: { | ||
| const { workflowId } = input.destination; | ||
| const res = await makeRequestWithRetry(() => | ||
| request.request<{ | ||
| workflow: { | ||
| envVars: { key: string; value: string }[]; | ||
| }; | ||
| }>({ | ||
| ...req, | ||
| url: `/workflows/${workflowId}` | ||
| }) | ||
| ); | ||
|
|
||
| return (res.data.workflow.envVars ?? []).map((item) => ({ | ||
| key: item.key, | ||
| value: item.value | ||
| })); | ||
| } |
There was a problem hiding this comment.
Path traversal / SSRF risk via unsanitized
workflowId
workflowId is taken directly from destinationConfig and interpolated into the URL path without any format validation. A value such as ../env-groups/grp-abc123 would resolve to /v1/env-groups/grp-abc123, redirecting the request to a completely different Render API endpoint. An attacker who controls the sync configuration could use this to read or overwrite arbitrary Render resources covered by the API key — for example, silently patching an environment group that is shared across many services instead of the intended workflow. The same pattern appears in batchUpdateEnvironmentSecrets at line 188. The workflowId schema should enforce a safe-character pattern (e.g., alphanumeric plus hyphens) to prevent traversal.
There was a problem hiding this comment.
Fixed in commit f093671: added a regex constraint /^[a-zA-Z0-9-]+$/ on workflowId in the schema, which rejects any value containing /, ., or other path-separator characters before it ever reaches the URL construction.
| await request.get<TRawRenderWorkflow[]>(`${IntegrationUrls.RENDER_API_URL}/v1/workflows`, { | ||
| params: new URLSearchParams({ | ||
| ...(cursor ? { cursor: String(cursor) } : {}), | ||
| limit: String(perPage) | ||
| }), | ||
| headers: { | ||
| Authorization: `Bearer ${apiKey}`, | ||
| Accept: "application/json", | ||
| "Accept-Encoding": "application/json" | ||
| } | ||
| }) | ||
| ).data; |
There was a problem hiding this comment.
Missing error handling for unexpected API response shape
The workflow listing loop casts the Axios response directly to TRawRenderWorkflow[] without any guard on item.workflow. If the Render API returns a page item that is missing the nested workflow object (e.g., a differently shaped error or an empty cursor sentinel), accessing item.workflow.name at line 107 will throw an uncaught TypeError that propagates out of listRenderWorkflows. The listRenderServices function has the identical pattern for item.service, so this follows the existing style — but it is worth adding a filter or optional-chaining guard before the push to avoid a runtime crash that would surface as an opaque 500 to the caller.
There was a problem hiding this comment.
Good catch — pushed a follow-up commit (176d84a) that adds if (!item.workflow) return; before accessing item.workflow.name / item.workflow.id, matching the defensive pattern used in listRenderServices.
| z.object({ | ||
| scope: z.literal(RenderSyncScope.Workflow).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope), | ||
| workflowId: z.string().min(1, "Workflow ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.workflowId), | ||
| type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type) | ||
| }) |
There was a problem hiding this comment.
The
type field is required on the Workflow variant of RenderSyncDestinationConfigSchema, but none of the workflow code paths in render-sync-fns.ts (getSecrets, batchUpdateEnvironmentSecrets, redeployService) ever read destinationConfig.type for the Workflow scope. Callers must supply a value that has no effect, which is misleading. If the field genuinely has no semantic meaning for workflows, it should be omitted from this variant to keep the API surface honest.
| z.object({ | |
| scope: z.literal(RenderSyncScope.Workflow).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope), | |
| workflowId: z.string().min(1, "Workflow ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.workflowId), | |
| type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type) | |
| }) | |
| z.object({ | |
| scope: z.literal(RenderSyncScope.Workflow).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope), | |
| workflowId: z.string().min(1, "Workflow ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.workflowId) | |
| }) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
The type field uses the same RenderSyncType enum that the Service and EnvironmentGroup variants expose, and was included so the Workflow config object sits in the same discriminated-union shape as the others. In practice, workflows don't have a meaningful service-type distinction (they can't be redeployed), so the field is a placeholder for future parity. Removing it now would be a schema break. Happy to change it to optional (z.nativeEnum(...).optional()) if you prefer that over keeping it required.
There was a problem hiding this comment.
That context makes sense — keeping shape parity across union variants is a reasonable convention, and a schema break mid-PR is worth avoiding.
Making it optional (z.nativeEnum(RenderSyncType).optional()) is the right call here. It keeps the discriminated union consistent while accurately signaling that the field has no semantic effect on workflows. Required fields that are silently ignored are the sharper footgun of the two.
| z.object({ | |
| scope: z.literal(RenderSyncScope.Workflow).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope), | |
| workflowId: z.string().min(1, "Workflow ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.workflowId), | |
| type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type) | |
| }) | |
| scope: z.literal(RenderSyncScope.Workflow).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope), | |
| workflowId: z.string().min(1, "Workflow ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.workflowId), | |
| type: z.nativeEnum(RenderSyncType).optional().describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type) |
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7cf24903dc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| url: `/workflows/${workflowId}`, | ||
| data: { envVars } |
There was a problem hiding this comment.
Use a supported Render workflow env-var update path
For workflow-scoped syncs, every sync and deletion path calls this branch to send PATCH /workflows/{workflowId} with envVars, but Render's Update workflow API only documents workflow metadata fields such as name, buildConfig, runCommand, and autoDeployTrigger; envVars is documented on workflow creation, not update. In environments using the new workflow scope, this means Infisical will either get a Render validation error or a successful metadata-only update while the secrets never change.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Valid concern worth flagging. The PATCH /workflows/{id} endpoint in Render's docs covers metadata fields, while env vars may need a sub-resource path (e.g. PUT /workflows/{id}/env-vars) consistent with how the service scope uses PUT /services/{id}/env-vars. I can't verify the exact endpoint without a live Render account. The listing change (workflows appearing in the UI) is unambiguously correct; the sync write path may need updating once tested against the live API. Happy to update if a maintainer can confirm the right endpoint, or I can flag it in the PR description as needing a Render account to verify.
| }) | ||
| ); | ||
|
|
||
| return (res.data.workflow.envVars ?? []).map((item) => ({ |
There was a problem hiding this comment.
Read workflow details from the actual response body
When a workflow-scoped sync/import runs, this reads res.data.workflow.envVars, but GET /v1/workflows/{workflowId} returns the workflow details for that ID rather than the paginated list shape used by GET /v1/workflows (the wrapper with { workflow, cursor } is only used by list responses). For any workflow destination, res.data.workflow is therefore undefined and the sync fails before it can update or import secrets.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in commit f093671: changed res.data.workflow.envVars to res.data.envVars since GET /v1/workflows/{id} returns the workflow object directly at the top level (same pattern as GET /v1/env-groups/{id}), not wrapped under a nested workflow key.
…alidate workflowId format
GET /v1/workflows/{id} returns the workflow object directly (same pattern
as GET /v1/env-groups/{id} returning envVars at the top level), not wrapped
under a nested `workflow` key. Reading res.data.workflow.envVars would always
be undefined; fixed to res.data.envVars.
Also added a regex constraint on workflowId to allow only alphanumeric
characters and hyphens, preventing path traversal in the URL.
|
Pushed a follow-up commit (f093671) that addresses two of the Greptile flags: Response shape bug (P1): Path traversal (P1): Added a regex constraint On the other P1 flag (PATCH endpoint not accepting On P2 ( |
Render occasionally returns page items without a nested workflow object (e.g. empty cursor sentinels). Accessing item.workflow.name without a guard caused a TypeError that propagated out of listRenderWorkflows.
176d84a to
e4dfc91
Compare
What
Adds support for syncing secrets to Render workflow services.
Render workflows are a separate resource from regular services. They live under
/v1/workflows(not/v1/services) and use a different env var API: env vars are read fromGET /v1/workflows/{id}and written viaPATCH /v1/workflows/{id}. The oldlistRenderServicescall missed them entirely, so they never appeared in the Service selector.Changes
workflowscope inRenderSyncScopelistRenderWorkflowsfunction that pages throughGET /v1/workflowsGET /:connectionId/workflowsendpoint on the Render connection router so the UI can populate the workflow selectorgetSecrets,syncSecrets, andremoveSecretsall handle theworkflowscope:GET /v1/workflows/{id}+ extractworkflow.envVarsPATCH /v1/workflows/{id}with the fullenvVarslist (full replacement, no per-key deletes needed)autoRedeployServicesis a no-op for workflows since they have no deploy endpointFixes #6409
Checklist