feat(variable): bulk-edit variables in $EDITOR with a reviewed diff - #1163
Merged
Conversation
variable editAdds `railway variable edit`. It opens the service's variables in $EDITOR as a dotenv document, diffs what you saved against what was there, prints the plan, and asks before writing anything: saving and quitting produces a plan, not an apply, and a non-zero exit from the editor aborts without touching anything. The snapshot comes from the stored user variables, via `variables(unrendered: true)`, with `isSealed` taken from the environment's variable connection. The rendered deploy-time map is the wrong source here: it merges Railway-provided variables in with the user's own and withholds sealed values, so an editor built on it would offer RAILWAY_SERVICE_NAME as editable text and show a sealed variable as absent. - Railway-provided variables are written as comments and excluded from the diff. Reserved names are rejected on parse and again before apply, so a pasted RAILWAY_* line cannot reach `variableCollectionUpsert`, which would otherwise persist a shadow row that the deploy-time merge silently overrides. - Sealed variables round-trip as <sealed>. Left alone they produce no diff entry and the stored ciphertext is untouched; replaced, they rotate. Typing <sealed> by hand on a variable that is not sealed is an error rather than a silent no-op. - Values are redacted in the plan unless --reveal is passed. The temp file is written 0600: it holds every plaintext value and would otherwise sit world-readable for as long as the editor is open. Deletions need --confirm-destructive when nobody is at the terminal to answer the prompt, matching how `config apply` treats destructive change. Edits that cannot be applied are rejected before the plan is printed, so the user is never asked to confirm something that will then fail. `--demo` runs the whole loop against an offline fixture, with no project link or API call, for trying the flow out. Co-authored-by: Cursor <cursoragent@cursor.com>
futurepastori
force-pushed
the
victor/vars-edit
branch
from
September 2, 2026 14:46
7de7a97 to
aa823e9
Compare
…t cleanup The editor UX was tried by hand; the safety properties were only asserted on the in-memory diff. Drive the GraphQL layer with MockBackboard so Railway- provided keys stay out of the editable map, sealed vars arrive as <sealed>, and apply emits the upsert/delete payloads we claimed. Also pin --confirm-destructive and prove a non-zero editor still unlinks the 0600 temp file. Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Adds
railway variable edit: opens the service's variables in$EDITORas adotenv document, diffs what you saved against what was there, prints an
IaC-style plan, and asks before writing anything. Saving and quitting is not
consent —
:wqgets you a plan, not an apply.What the editor shows you
The first cut read its snapshot from
variablesForServiceDeployment— therendered map a deploy receives. That map merges Railway-provided variables in
with the user's own and drops sealed values, which made for two bad outcomes:
RAILWAY_SERVICE_NAMEand friends were offered as editable text, and a sealedvariable was invisible, indistinguishable from one that had been deleted.
It now reads the stored user variables through
variables(unrendered: true)and takes
isSealedfrom the environment's variable connection.diff. Reserved names are rejected on parse and again before apply, so a
pasted
RAILWAY_*line cannot reachvariableCollectionUpsert— which wouldotherwise persist a shadow row that the deploy-time merge silently overrides.
<sealed>. Left alone they produce no diffentry at all and the stored ciphertext is untouched; replaced, they rotate.
Typing
<sealed>by hand on a variable that is not sealed is an error ratherthan a silent no-op.
--revealprints them.Guardrails
0600. It holds every plaintext value, and it waslanding in a world-readable path.
--confirm-destructivewhen there is no one at the terminal toanswer the prompt —
--yes, a pipe, or an agent session. This matches howconfig applytreats destructive change.are never asked to confirm something that will then fail.
Flags
--yes,--skip-deploys,--reveal,--confirm-destructive,--demo, plusthe usual
--service/--environment/--project.Testing
cargo test— 1324 pass, 11 of them new around reserved-name exclusion, sealedpreserve/rotate, the hand-typed
<sealed>rejection, dotenv round-tripping, andtemp file permissions.
cargo fmt --checkandcargo clippy --all-targets --all-featuresare clean for the touched files.Not yet covered: variable references (
${{...}}) round-trip as their rawunrendered text, which is correct for storage but means the editor shows the
reference rather than its value.