feat: env-updater plugin for cross-platform env sync#756
Conversation
Adds a new secrets provider that syncs environment variables across: - Local .env files - Doppler (via Doppler CLI) - Railway (via Railway CLI) - GitHub Secrets (via gh CLI) The provider extends the existing secrets architecture with a unified push/pull interface that updates all configured targets at once. Closes profullstack#710 Usage: sh1pt secret push --provider env-updater
Greptile SummaryThis PR introduces
Confidence Score: 3/5Not safe to merge as-is: the hardcoded githubOwner fallback to profullstack will silently push secrets to the wrong GitHub organization for any user who omits that config field. The githubOwner field defaults to the package author's GitHub org name instead of throwing like githubRepo does. A user who configures only githubRepo: my-app will push all their secrets to profullstack/my-app without any error or warning. The main and types fields in package.json pointing to TypeScript source remain unaddressed. packages/secrets/env-updater/src/index.ts (line 109 githubOwner default) and packages/secrets/env-updater/package.json (main and types fields). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[sh1pt secret push\n--provider env-updater] --> B[push handler]
B --> C[Build secretMap\nfrom SecretRef array]
C --> D[writeEnvFile\nmerge into local .env]
D --> E{dopplerProject\nconfigured?}
E -- Yes --> F[updateDoppler\nspawnSync doppler secrets set]
E -- No --> G{railwayService\nconfigured?}
F --> G
G -- Yes --> H[updateRailway\nspawnSync railway variables set]
G -- No --> I{githubRepo\nconfigured?}
H --> I
I -- Yes --> J[updateGitHubSecrets\nspawnSync gh secret set\nvalue via stdin]
I -- No --> K[Return results summary]
J --> K
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[sh1pt secret push\n--provider env-updater] --> B[push handler]
B --> C[Build secretMap\nfrom SecretRef array]
C --> D[writeEnvFile\nmerge into local .env]
D --> E{dopplerProject\nconfigured?}
E -- Yes --> F[updateDoppler\nspawnSync doppler secrets set]
E -- No --> G{railwayService\nconfigured?}
F --> G
G -- Yes --> H[updateRailway\nspawnSync railway variables set]
G -- No --> I{githubRepo\nconfigured?}
H --> I
I -- Yes --> J[updateGitHubSecrets\nspawnSync gh secret set\nvalue via stdin]
I -- No --> K[Return results summary]
J --> K
Reviews (2): Last reviewed commit: "fix: address security audit findings" | Re-trigger Greptile |
| async function writeEnvFile(file: string, secrets: Record<string, string>): Promise<void> { | ||
| const lines = Object.entries(secrets).map(([key, value]) => `${key}=${value}`); | ||
| await writeFile(file, lines.join('\n') + '\n', 'utf8'); | ||
| } |
There was a problem hiding this comment.
Unquoted values in
writeEnvFile break roundtrip for values with spaces or special characters
readEnvFile strips leading/trailing quotes from values (line 35), but writeEnvFile writes values bare: KEY=hello world. Tools that source the file as shell — e.g., source .env — will misparse unquoted values with spaces, # comments, or embedded = characters. Values should be written with double-quote wrapping, with any embedded double quotes escaped, to preserve round-trip fidelity.
| "main": "./src/index.ts", | ||
| "types": "./src/index.ts", |
There was a problem hiding this comment.
main and types point to TypeScript source, not compiled output
"main": "./src/index.ts" will fail under standard Node.js module resolution since Node cannot import .ts files natively. The types field should also point to a generated .d.ts declaration file, not the source. There are also no scripts (e.g., build, test) and no devDependencies for typescript or vitest, so there is no defined path to produce the compiled artefacts the main/types fields should reference.
- Replace execSync with spawnSync + args arrays (fixes command injection) - Pass GitHub secrets via stdin instead of temp files (fixes secret leak) - Merge into existing .env instead of overwriting (fixes destructive overwrite)
|
✅ Security audit findings addressed:
|
Closes #710
Adds @profullstack/secrets-env-updater — syncs env vars across .env, Doppler, Railway, and GitHub Secrets with one command.
Usage: sh1pt secret push --provider env-updater