Skip to content

feat(build): add support for marking env vars as secrets in syncEnvVars#2417

Open
julienvanbeveren wants to merge 5 commits intotriggerdotdev:mainfrom
julienvanbeveren:feat/sync-env-secrets
Open

feat(build): add support for marking env vars as secrets in syncEnvVars#2417
julienvanbeveren wants to merge 5 commits intotriggerdotdev:mainfrom
julienvanbeveren:feat/sync-env-secrets

Conversation

@julienvanbeveren
Copy link
Copy Markdown

@julienvanbeveren julienvanbeveren commented Aug 19, 2025

✅ Checklist

  • I have followed every step in the contributing guide
  • The PR title follows the convention.
  • I ran and tested the code works

Testing

  • Verified all changed packages build successfully (@trigger.dev/core, @trigger.dev/build, trigger.dev CLI)
  • Ran pnpm run typecheck --filter webapp — passes cleanly

Changelog

Added support for the isSecret flag on per-variable items in the syncEnvVars build extension, wiring it end-to-end from user callbacks through the build manifest, CLI deploy, API import route, and into the EnvironmentVariablesRepository storage layer.

Bug fixes (from Devin Review and maintainer feedback):

  1. Secrets data no longer dropped in build manifest pipeline — Added secrets and parentSecrets fields to BuildLayer.deploy interface, the build manifest deploy.sync schema, and applyLayerToManifest. The syncEnvVars extension now passes result.secrets and result.parentSecrets through context.addLayer().

  2. Deploy call sites now forward secrets — Updated syncEnvVarsWithServer call sites in both deploy.ts and workers/build.ts to read secrets/parentSecrets from the build manifest and pass them to the API client.

  3. Per-variable isSecret now reaches storage — Extended the CreateEnvironmentVariables schema to support an optional isSecret on each variable item. Updated EnvironmentVariablesRepository.create() to use per-item isSecret (falling back to the top-level options.isSecret) for inheritance lookup, skip-check, update writes, and insert writes. Made removeBlacklistedVariables generic so the isSecret type is preserved through filtering.


Screenshots

N/A — backend-only changes, no UI impact.

💯

Link to Devin session: https://app.devin.ai/sessions/97ecc669a2624b7c82127d1678670773
Requested by: @matt-aitken

- Added isSecret flag to SyncEnvVarsBody type
- Updated CLI's syncEnvVarsWithServer to pass secret flags
- Modified backend API endpoint to handle secret flags
- Added documentation for the new feature
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Aug 19, 2025

🦋 Changeset detected

Latest commit: 1edd66e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 29 packages
Name Type
trigger.dev Minor
@trigger.dev/build Minor
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
references-realtime-hooks-test Patch
references-realtime-streams Patch
references-telemetry Patch
@trigger.dev/python Minor
@trigger.dev/core Minor
@trigger.dev/react-hooks Minor
@trigger.dev/redis-worker Minor
@trigger.dev/rsc Minor
@trigger.dev/schema-to-json Minor
@trigger.dev/sdk Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql Patch
@internal/zod-worker Patch
@internal/sdk-compat-tests Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 19, 2025

Walkthrough

Adds per-variable secret support across sync/import flows. New changeset added. API schema and client types gain optional secrets, parentSecrets, and parentVariables fields. The webapp import route reads body.secrets / body.parentSecrets and sets isSecret on imported variables; EnvironmentVariablesRepository.create signature now accepts isSecret. The build extension syncEnvVars accepts isSecret on items and tracks secrets and parentEnvSecrets alongside env / parentEnv. The CLI exposes and forwards secrets and parentSecrets. Some template/string formatting tweaks were also applied.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title clearly and concisely describes the main feature introduced, specifying that support for marking environment variables as secrets has been added to the syncEnvVars function.
Description check ✅ Passed The pull request description includes all required sections: a completed checklist, testing steps, and a detailed changelog explaining the changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]

This comment was marked as resolved.

@matthova
Copy link
Copy Markdown

Late to the game, but would love to see this as well. Curious if the original work is still good enough or if this needs a new attempt with so much changing in the last year-or-so

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

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.

🔴 Secrets data dropped in syncEnvVars: never stored in build manifest

The callSyncEnvVarsFn function correctly computes secrets and parentSecrets from user callbacks (lines 148-224), but the syncEnvVars function silently drops them when calling context.addLayer() at lines 117-124 — only env and parentEnv are passed in the deploy object. Additionally, the BuildLayer interface (packages/core/src/v3/build/extensions.ts:63-66) and the build manifest schema (packages/core/src/v3/schemas/build.ts:52-57) don't define secrets/parentSecrets fields, so there's no way to carry this data through the manifest pipeline. As a result, the entire "secret flag on variables" feature advertised by this PR is non-functional — isSecret will always resolve to false on the server side (body.secrets?.[key] ?? false).

(Refers to lines 117-124)

Prompt for agents
The secrets data computed in callSyncEnvVarsFn is dropped at line 117 because context.addLayer() only receives env and parentEnv. To fix this:

1. Add secrets and parentSecrets fields to BuildLayer.deploy in packages/core/src/v3/build/extensions.ts (the BuildLayer interface around line 63).
2. Add secrets and parentSecrets fields to the deploy.sync schema in packages/core/src/v3/schemas/build.ts (around line 52-57).
3. In packages/build/src/extensions/core/syncEnvVars.ts, pass result.secrets and result.parentSecrets through context.addLayer() in the deploy object.
4. In packages/cli-v3/src/build/extensions.ts, handle the new secrets/parentSecrets fields when processing layers (around lines 147-186) and store them in the manifest.
5. In packages/cli-v3/src/commands/deploy.ts, read the secrets from buildManifest.deploy.sync and pass them to syncEnvVarsWithServer (see BUG-0002).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

🔴 syncEnvVarsWithServer called without secrets/parentSecrets arguments in deploy.ts

The syncEnvVarsWithServer function signature was updated to accept secrets and parentSecrets parameters (lines 768-769), but the call site at lines 472-478 only passes childVars and parentVars, omitting the new parameters. Even once the build manifest carries secrets data (see the related manifest schema issue), this call site still needs to be updated to pass them. The same issue exists in the other call site at packages/cli-v3/src/commands/workers/build.ts:272-278 where the function signature wasn't even updated.

(Refers to lines 472-478)

Prompt for agents
The call to syncEnvVarsWithServer at line 472 needs to also pass the secrets and parentSecrets from the build manifest. Once the manifest schema is updated to carry secrets data (see the related BuildLayer/manifest issue), read secrets and parentSecrets from buildManifest.deploy.sync and pass them as the 6th and 7th arguments.

Also update the duplicate call site in packages/cli-v3/src/commands/workers/build.ts around line 272 and its local syncEnvVarsWithServer function at line 448 to accept and forward secrets/parentSecrets as well.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@matt-aitken
Copy link
Copy Markdown
Member

The per-variable isSecret flag added in api.v1.projects.$projectRef.envvars.$slug.import.ts never reaches storage — this PR is currently a no-op on the import endpoint.

The problem

The route attaches isSecret to each item in the variables array:

variables: Object.entries(body.variables).map(([key, value]) => ({
  key,
  value,
  isSecret: body.secrets?.[key] ?? false,
})),

But EnvironmentVariablesRepository.create() only reads options.isSecret (a single top-level flag applying to all variables in the call). It never reads variable.isSecret from the array items. The CreateEnvironmentVariables schema in apps/webapp/app/v3/environmentVariables/repository.ts confirms this — variables is typed as { key, value }[] with no per-item isSecret. The extra field passes TypeScript via structural assignability and is silently discarded at runtime.

Net effect for an existing variable that was previously a secret:

  • The update branch (lines 214–231) only writes isSecret when options.isSecret !== undefined — so the existing value is preserved regardless of what's in body.secrets.
  • New variables get isSecret: undefined (or whatever is inherited from the parent env), again ignoring body.secrets.

So passing isSecret: true does not promote a var to a secret, and passing isSecret: false does not un-secret one. The wiring stops at the route boundary.

Suggested fix

Move isSecret onto the per-variable item all the way down:

  1. Extend the CreateEnvironmentVariables schema so variables items carry an optional isSecret: z.boolean().optional().

  2. Inside create()'s per-variable loop, compute const perItemIsSecret = variable.isSecret ?? options.isSecret; and use that in place of options.isSecret for the inheritance lookup, the canSkip check, the update's conditional isSecret write, and effectiveIsSecret on insert.

  3. In the import route, drop the ?? false fallback:

    isSecret: body.secrets?.[key],

    Keeping it as undefined when the key is missing from secrets preserves the existing tri-state semantics: undefined = leave existing value / inherit from parent, true = mark as secret, false = explicitly un-secret. With ?? false, every import without a secrets map would un-secret every previously-secret variable.

Why per-item rather than two calls bucketed by isSecret

A split-call approach (one call with options.isSecret: true, another with options.isSecret: false) breaks the parent-env inheritance at lines 162–177 of the repository — both calls would pass an explicit boolean, so options.isSecret === undefined is never true and the parentEnvironmentId lookup never runs. The route deliberately passes parentEnvironmentId for exactly that fallback. Per-item preserves undefined for variables not named in body.secrets, so inheritance still works for them.

It's also a smaller change — create() already iterates one variable at a time, so it's just swapping which field is read inside the existing loop instead of adding a second transaction and another project/variable lookup pass.

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.

3 participants