Skip to content

feat(variable): bulk-edit variables in $EDITOR with a reviewed diff - #1163

Merged
futurepastori merged 2 commits into
masterfrom
victor/vars-edit
Sep 2, 2026
Merged

feat(variable): bulk-edit variables in $EDITOR with a reviewed diff#1163
futurepastori merged 2 commits into
masterfrom
victor/vars-edit

Conversation

@futurepastori

@futurepastori futurepastori commented Sep 1, 2026

Copy link
Copy Markdown
Member

Adds railway variable edit: opens the service's variables in $EDITOR as a
dotenv 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 — :wq gets you a plan, not an apply.

railway variable edit
railway variable edit --demo     # offline fixture, no project or API needed

What the editor shows you

The first cut read its snapshot from variablesForServiceDeployment — the
rendered 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_NAME and friends were offered as editable text, and a sealed
variable was invisible, indistinguishable from one that had been deleted.

It now reads the stored user variables through variables(unrendered: true)
and takes isSealed from the environment's variable connection.

  • 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 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 rather
    than a silent no-op.
  • Values are redacted in the plan by default. --reveal prints them.

Guardrails

  • The temp file is written 0600. It holds every plaintext value, and it was
    landing in a world-readable path.
  • Deletions need --confirm-destructive when there is no one at the terminal to
    answer the prompt — --yes, a pipe, or an agent session. This matches how
    config apply treats destructive change.
  • Edits that cannot be applied are rejected before the plan is printed, so you
    are never asked to confirm something that will then fail.
  • A non-zero exit from the editor aborts without touching anything.

Flags

--yes, --skip-deploys, --reveal, --confirm-destructive, --demo, plus
the usual --service / --environment / --project.

Testing

cargo test — 1324 pass, 11 of them new around reserved-name exclusion, sealed
preserve/rotate, the hand-typed <sealed> rejection, dotenv round-tripping, and
temp file permissions. cargo fmt --check and cargo clippy --all-targets --all-features are clean for the touched files.

Not yet covered: variable references (${{...}}) round-trip as their raw
unrendered text, which is correct for storage but means the editor shows the
reference rather than its value.

@futurepastori futurepastori added the release/minor Author minor release label Sep 2, 2026
@futurepastori futurepastori changed the title feat(variable): prototype variable edit feat(variable): bulk-edit variables in $EDITOR with a reviewed diff Sep 2, 2026
@futurepastori futurepastori reopened this Sep 2, 2026
Adds `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>
…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>
@futurepastori
futurepastori merged commit 69546b1 into master Sep 2, 2026
7 checks passed
@futurepastori
futurepastori deleted the victor/vars-edit branch September 2, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/minor Author minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant