Skip to content

feat(secret-sync): add Render workflow scope to secret sync#6906

Open
jaydeep-pipaliya wants to merge 3 commits into
Infisical:mainfrom
jaydeep-pipaliya:fix/render-workflow-sync
Open

feat(secret-sync): add Render workflow scope to secret sync#6906
jaydeep-pipaliya wants to merge 3 commits into
Infisical:mainfrom
jaydeep-pipaliya:fix/render-workflow-sync

Conversation

@jaydeep-pipaliya

@jaydeep-pipaliya jaydeep-pipaliya commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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 from GET /v1/workflows/{id} and written via PATCH /v1/workflows/{id}. The old listRenderServices call missed them entirely, so they never appeared in the Service selector.

Changes

  • New workflow scope in RenderSyncScope
  • listRenderWorkflows function that pages through GET /v1/workflows
  • New GET /:connectionId/workflows endpoint on the Render connection router so the UI can populate the workflow selector
  • getSecrets, syncSecrets, and removeSecrets all handle the workflow scope:
    • reads use GET /v1/workflows/{id} + extract workflow.envVars
    • writes use PATCH /v1/workflows/{id} with the full envVars list (full replacement, no per-key deletes needed)
    • autoRedeployServices is a no-op for workflows since they have no deploy endpoint

Fixes #6409

Checklist

  • I have read the contributing guide
  • My changes follow the project coding style
  • I have performed a self-review of my changes
  • I added tests where applicable (no test coverage for connection fns in this area)

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
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a workflow scope to the Render secret sync integration, allowing Infisical to read and write env vars on Render workflow resources via GET/PATCH /v1/workflows/{id}. The new scope correctly uses full-replacement semantics (matching how the Service scope uses a PUT), a new listRenderWorkflows pagination helper, and a router endpoint for the UI to populate the workflow selector.

  • render-sync-fns.ts handles getSecrets, batchUpdateEnvironmentSecrets, redeployService (no-op), and removeSecrets for the new scope — all logically consistent with Render's PATCH-replace-all contract.
  • render-connection-fns.ts adds listRenderWorkflows mirroring the existing listRenderServices pagination loop; the new router endpoint and service method follow established patterns exactly.
  • The workflowId field in destinationConfig is validated only for presence (min(1)), not format, so it is interpolated into URL paths without sanitization — a path-traversal issue on the Render API domain.

Confidence Score: 3/5

The workflow sync logic itself is sound, but the unsanitized workflowId reaching Render API URL paths should be addressed before merging to production.

The core sync flow is implemented correctly — full-replacement PATCH semantics, proper no-op for redeploy, and consistent pagination. The main concern is that workflowId is interpolated into HTTP paths without format validation, enabling path traversal within the Render API domain.

render-sync-fns.ts and render-sync-schemas.ts need attention for the unsanitized workflowId used in URL construction.

Security Review

  • Path traversal via workflowId (render-sync-fns.ts lines 110, 193): workflowId is sourced from user-supplied destinationConfig and interpolated directly into URL paths (e.g., /workflows/${workflowId}) with only a min(1) length check. A crafted value such as ../env-groups/grp-abc123 resolves to a different Render API endpoint, allowing a user to read or overwrite Render resources outside the intended workflow scope using their own API key. No path canonicalization or allowlist pattern validation is applied before the value reaches the HTTP client.

Important Files Changed

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)

  1. backend/src/services/secret-sync/render/render-sync-fns.ts, line 356-358 (link)

    P2 Workflow deletions silently skipped when disableSecretDeletion is enabled

    The syncSecrets Step 4 guard only invokes the explicit per-key delete for EnvironmentGroup scope. For Workflow scope this is intentional because the PATCH is a full replacement — but when disableSecretDeletion is true, the logic in Step 1 preserves the secret in finalEnvVars, so the PATCH correctly keeps it. However when disableSecretDeletion is false, secrets that were removed from Infisical go into secretsToDelete (not finalEnvVars), and since the PATCH only sends finalEnvVars, 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

Comment on lines +101 to +118
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
}));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +92 to +103
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +30 to 34
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)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +193 to +194
url: `/workflows/${workflowId}`,
data: { envVars }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@jaydeep-pipaliya

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit (f093671) that addresses two of the Greptile flags:

Response shape bug (P1): GET /v1/workflows/{id} returns the workflow object directly at the top level (same pattern as GET /v1/env-groups/{id} which returns envVars directly). I was incorrectly reading res.data.workflow.envVars; fixed to res.data.envVars.

Path traversal (P1): Added a regex constraint /^[a-zA-Z0-9-]+$/ on workflowId in the Zod schema so only alphanumeric + hyphen values are accepted.

On the other P1 flag (PATCH endpoint not accepting envVars): the issue comments include a deep-dive into Render's OpenAPI spec by @Abdul-Moiz31 that explicitly states env vars on a workflow are writable via PATCH /v1/workflows/{id} (operationId updateWorkflow). I'm keeping that path.

On P2 (type field unused for Workflow): that's correct — I kept it for API surface consistency with the Service and EnvironmentGroup scopes (both require type), but happy to drop it from the Workflow variant if reviewers prefer a leaner schema.

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.
@jaydeep-pipaliya jaydeep-pipaliya force-pushed the fix/render-workflow-sync branch from 176d84a to e4dfc91 Compare June 18, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Render workflow service are not displayed in secret-sync select

1 participant