Skip to content

feat: end-to-end TFC migration pipeline with guided init and import - #16

Draft
hllvc wants to merge 55 commits into
masterfrom
feat/improvements
Draft

feat: end-to-end TFC migration pipeline with guided init and import#16
hllvc wants to merge 55 commits into
masterfrom
feat/improvements

Conversation

@hllvc

@hllvc hllvc commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Turns the migrator from a hand-driven terraform apply plus manual editing into one guided pipeline: ./sg-migrate.sh all configures, preflights, extracts and transforms the workspaces, merges Variable Sets, converts and validates the payloads, and bulk-imports them into per-project workflow groups. The transformer gains per-project output, a migration summary, API-based state export, VCS trigger mapping and execution-preset support.

Suggested review order: transformer/terraform-cloud/locals.tf (the TFC to SG mapping, where payload shape is decided), then scripts/migrate.sh (phase orchestration, import parsing, resume state), then scripts/lib/ (wizard, preflight, checklist). The rest is glue and docs.

Changes

  • Transformer: one sg-payload.<project>.json per TFC project with WorkflowGroup.name = tfc-<project>; migration-summary.{md,json}; state pulled from the TFC API instead of a per-workspace terraform init; TFC VCS settings mapped to VCSTriggers; sensitive variables skipped and reported; ignoreVarPatterns strips TFC_*/TFE_* variables.
  • Defaults and presets: SGDefault* inputs plus typed workspaceOverrides. SGTerraformVersionSource and nullable SGDefaultTerraformVersion/SGDefaultRunnerConstraints leave keys out of the payload so the SG API fills them from the org's execution preset.
  • Orchestrator: sg-migrate.sh runs scripts/migrate.sh in the Docker image (native fallback) with init|preflight|apply|enrich|convert|validate|import|triggers|checklist|all|clean|completion. Phases resume via .sg/state.json, filter with --project/--workspace; import --dry-run stops at the plan.
  • Guided init and preflight: the wizard discovers TFC orgs/projects/workspaces and SG connectors, runner groups and the execution preset; preflight verifies tokens, referenced connectors/secrets/runner groups, kind/prefix consistency and the workspace selection before anything runs.
  • Import: creates missing tfc-<project> groups, parses sg-cli output (it exits 0 on per-workflow failures), retries a pin above SG's 1.5.7 Terraform ceiling with the fallback or preset, maps known API errors to a hint naming the tfvars field, and writes export/post-import-checklist.md with CHANGE_ME secret stubs for sensitive variables.
  • Validation and docs: schema/sg-payload.schema.json with scripts/validate_payload.sh; README rewritten for the orchestrated flow.

Testing

  • ./sg-migrate.sh all against a multi-project TFC org: wizard, preflight, one payload per project, groups created, workflows imported, checklist written.
  • Re-run all after a failed import: unchanged phases are skipped, only files with failures are re-imported.
  • A workspace pinned above 1.5.7 imports via the fallback and lands in export/terraform-version-fallbacks.log; with SGDefaultTerraformVersion = null the preset applies.
  • terraform fmt -check on the transformer and ./scripts/validate_payload.sh export/sg-payload.*.json pass.

Risks

  • Output layout changed: sg-payload.json became one file per project and WorkflowGroup.name is tfc-<project> rather than the TFC project ID.
  • Import writes to the SG org before the bulk create (workflow groups, placeholder secrets); --no-create-groups and --no-secret-stubs opt out.
  • Payloads with terraformVersion or RunnerConstraints omitted depend on the org's execution preset being set as intended.

Deployment Notes

  • Env vars: SG_API_TOKEN, SG_ORG, optional SG_BASE_URL, and TFE_TOKEN (preferred over terraform login); tuning knobs are in the README.
  • The Docker image is built locally on first run (--build to rebuild); nothing is pushed to a registry.

hllvc added 4 commits June 24, 2026 19:05
- pull workspace state from the TFC API instead of per-workspace init
- write migration-summary.{md,json}; replace workspace.tmpl with summary.tmpl
- map TFC vcs settings to SG VCSTriggers (push, PR speculative, file filters)
- add SGDefault*/workspaceOverrides for vcs auth, source kind and triggers
Validate generated payloads against schema/sg-payload.schema.json via yajsv.
- sg-migrate.sh + Dockerfile run the full pipeline inside a container
- scripts/migrate.sh: init/apply/enrich/convert/validate/import/triggers/all
- merge TFC variable sets, parallel convert/import, register VCS triggers
- move convert_hcl_to_json.sh into scripts/
@hllvc hllvc self-assigned this Sep 3, 2026
hllvc added 25 commits September 7, 2026 07:57
sg-cli exits 0 even when individual workflows fail, so the importer now
parses its output. Workflows rejected because their pinned Terraform
version is above SG's managed ceiling (1.5.7, last MPL/FOSS release) are
re-imported with SGDefaultTerraformVersion, the payload is patched in
place, and each case is logged to export/terraform-version-fallbacks.log
with a printed notice. Any other per-workflow failure now fails the run.

Also: sg_retry captured the wrong exit status (always 0), definitive
HTTP 4xx responses are no longer retried, the trigger pass skips
workflows that were never created, and failure output no longer echoes
the API token.
'sg-migrate.sh completion bash|zsh' prints a completion script for the
current shell session; 'init' prints the source line for the user's
shell. Runs natively like 'clean'.
Running sg-migrate.sh without a command used to start the whole
pipeline; it now prints the help menu, locally, without touching Docker.
Usage and hints show the name the user invoked (./sg-migrate.sh) instead
of the in-container script path.
Without TFE_TOKEN or a terraform login credentials file, apply used to
warn and then fail deep inside terraform with "Invalid provider
configuration". Both the host entrypoint and migrate.sh now stop with a
clear message before anything runs. Add an explicit provider "tfe" block
so tfHostname also applies to the provider, not just the state export.
An expired 'terraform login' session (or a bad TFE_TOKEN) used to fail
deep inside terraform. Resolve the token the tfe provider will use, in
its own precedence, and check it against /api/v2/account/details first;
stop with a clear message on 401/403 or when the host is unreachable.
Every workflow was hardcoded to shared runners unless overridden per
workspace. Add SGDefaultRunnerConstraints (default shared) so all
workflows can be put behind a private runner group in one place;
workspaceOverrides[name].RunnerConstraints still wins per workspace.
Validated: type is shared|private, and private requires names.
Move the TFC/SG API helpers out of migrate.sh into scripts/lib/ and add
the building blocks the upcoming init wizard, preflight and checklist
need: interactive prompt helpers (tty or scripted answers), cached
tfvars reads, paginated TFC listing (orgs/projects/workspaces) and SG
lookups for integrations, runner groups, workflows and secrets. The
enrich script now shares the same token resolution as the provider.
Also stop turning curl connection failures into "000000" status codes.
'init' now walks through the configuration instead of copying the
example file: it lists the TFC organisations, projects and workspaces
the token can see, the SG VCS and cloud connectors and runner groups,
and derives the source kind and repo prefix from the chosen connector.
Every step falls back to free text when a token is missing or an API
call fails, and non-interactive runs keep the template behaviour.
The generated terraform.tfvars keeps the example's order and comments;
re-running the wizard prefills the current values and keeps a .bak.
Verify up front what used to fail minutes later inside terraform or as
API 400s: TFC token and org, that the workspace selection matches
anything, SG token and org, every connector / secret / runner group
referenced in terraform.tfvars (defaults and workspaceOverrides),
SGDefaultSourceConfigDestKind and SGDefaultTerraformVersion format, the
exportPath vs export-dir mismatch, and override keys that match no
workspace. Runs automatically for apply, import and all; also available
as 'preflight'; --skip-preflight bypasses it.
'all' now records each phase's inputs in .sg/state.json and skips phases
whose inputs have not changed, so a failed run resumes instead of
re-running terraform apply. Import records per payload file which
workflows landed and which failed; unchanged, fully imported files are
skipped and files with failures are re-imported (sg-cli updates
existing workflows in place). --fresh discards the state. --project
and --workspace restrict every phase to a subset; apply maps
--workspace to the workspacenames variable.
apply now prints a condensed migration summary (workflows per project,
sensitive variables skipped, unpinned Terraform versions, non-remote
execution modes, renames, failed state exports) instead of leaving it in
a file nobody opens. import shows a per-workflow table before the
confirmation prompt: create/update, Terraform version with the 1.5.7
fallback marked, runner, triggers, variable and secret counts.
'import --dry-run' stops after the table without touching anything.
Map the API messages users actually hit (unknown connector, missing
runner group, repository not reachable, bad approvers, invalid names or
VCS kind, rejected token, Terraform ceiling) to a one-line hint naming
the terraform.tfvars field to fix. Shown once per distinct hint, both
for direct API calls and for sg-cli's per-workflow failures; unknown
messages still show the raw response.
After import (or via 'checklist'), write export/post-import-checklist.md
listing everything the migration could not do by itself: secrets to
fill in, workflows that failed to import, Terraform version fallbacks
to verify, VCS triggers that failed, state that could not be exported,
non-remote execution modes and renamed workflows, with deep links into
the SG UI (SG_UI_URL). For every sensitive variable TFC would not
expose, create an SG secret with the value CHANGE_ME, reference it from
the workflow (env var or IaC input, ${secret::<name>}) and patch the
payload to match; --no-secret-stubs opts out. Trigger registration
results are now recorded in the run state.
The integrations list has no ResourceType; the kind (GITHUB_COM,
AWS_RBAC, ...) is Settings.kind. Map it from there, make the type
filters null-safe, and only treat msg/data as the item list when it is
actually an array.
_w_csv_json emitted "[]" twice when the list was empty (grep -v on empty
input trips the fallback under pipefail), so the generated file was not
valid HCL and preflight reported every field as missing. Validate the
file right after writing it, report an unparsable tfvars as such in
preflight, remember the SG org / API host chosen in the wizard so a new
shell does not need SG_ORG, pretty-print objects in the generated file
and separate menu values from their descriptions.
The hint used $SHELL, which is the login shell and was wrong for a bash
login shell running zsh, so the bash script got sourced into zsh and
broke on the first Tab. sg-migrate.sh now detects the parent shell and
'completion' with no argument prints the matching script; each script
also hands off to the other shell's version when sourced by mistake.
The per-workflow plan passed whatever sg_list_workflows returned to
jq --argjson and crashed when the response was not a plain array.
List helpers now always yield an array (also unwrapping data.Workflows)
and the plan guards the value. The wizard no longer asks for the
profile name (not required), the repo URL prefix, approvers or the
fallback Terraform version — they take sensible defaults and are edited
in terraform.tfvars. sg-migrate.sh says when it runs in Docker.
When terraform.tfvars is missing, all runs the wizard and then asks
whether to continue right away or edit the file first; only the
non-interactive template copy still stops, since it holds placeholders.
Wrap argument parsing and dispatch in main so bash reads the whole
script up front. The repo is bind-mounted into the container, and a
file edited during a long run was read half-old, half-new, ending in
"unexpected EOF while looking for matching quote" after the import had
already succeeded.
TFC_* / TFE_* variables (TFC_WORKSPACE_NAME, the TFC_AWS_* dynamic
credential settings, ...) only mean something inside Terraform Cloud.
A new ignoreVarPatterns input (default ["^TFC_", "^TFE_"]) drops matching
terraform and env variables from the payloads, from workspaces and from
variable sets, lists them in the migration summary and keeps them out of
the sensitive-variable list so no secret stubs are created for them.
The init wizard asks whether to strip them. Also fix the secrets link in
the checklist (orgs/<org>?tab=secrets).
The "^(AWS|AZURE|GCP)_" filter also matched AZURE_DEVOPS, so an Azure
DevOps VCS connector could be chosen as the cloud connector and only
schema validation caught it. Both pickers now use the exact kind lists
(VCS also accepts AZURE_DEVOPS_SP and GITLAB_OAUTH_SSH and maps them to
the source kind), are sorted by name, and preflight rejects a
DeploymentPlatformConfig kind outside the schema enum. The validator no
longer prints each failure twice.
Presets: SGTerraformVersionSource (carry|preset) plus nullable
SGDefaultTerraformVersion and SGDefaultRunnerConstraints. Keys left out of
the payload are filled from the org's execution preset by the SG API at
import. init offers the preset with the current value shown, preflight
reports it, the plan marks 'preset (...)' cells, the checklist explains what
changed, and a pin rejected above the 1.5.7 ceiling falls back to the preset
when the fallback is null.

Flow: numbered phases with timings, live progress lines for terraform and
the parallel phases, plain-language preflight with connector/kind/prefix
consistency checks, readable wizard review, per-file convert/validate lines,
a condensed post-import checklist with group links, and a neutral message
when the import is declined.

Fixes: init kept the previous repo URL prefix when the connector switched
provider (the prefix now comes from the TFC repositories); enrich re-ran on
every 'all' because convert changed its recorded hash; quotes in connector
names broke the generated tfvars; a RunnerConstraints override with names
failed HCL type unification.
@hllvc hllvc changed the title feat: dockerized migration pipeline with per-project payloads feat: end-to-end TFC migration pipeline with guided init and import Sep 8, 2026
sg_api_raw does the request and returns the body on any status, with
SG_HTTP_CODE set in the caller's shell; _sg_api keeps the logging and
the body-on-2xx-only behaviour on top of it.
…ty iacInputData.data

sg-cli 2.2.1 round-trips every bulk entry through sg-sdk-go v1.1.0's
Workflow struct, whose iacInputData.data map is tagged omitempty, so a
workspace with no Terraform variables reaches the API without the key
and is rejected with "VCSConfig.iacInputData.data: This field is
required."

import_bulk now splits each payload file: workflows with variables go
through sg-cli as before, the ones without are created straight from
the payload via POST .../wfs/ (PATCH when the name exists) followed by
the same state upload sg-cli does. The direct path prints sg-cli's
"Failed to create <name>: <code>: <body>" line on rejection, so the
ceiling fallback, error hints and checklist cover both paths.

Workaround only; TODO(sg-cli) markers show what to remove once sg-cli
ships with sg-sdk-go >= v1.5.7, where the field became a pointer and
"data": {} is sent as given.
…failed import

sg-cli's state upload sends a PUT without x-ms-blob-type, which Azure
Blob rejects (400 MissingRequiredHeader), and it only recognises a
literal "HTTP/1.1 200 OK" as success, so on Azure-backed environments
every workflow was created without its state while the run reported
"state: exported for every selected workspace".

import_bulk now reads sg-cli's per-workflow upload lines and re-uploads
every reported failure through sg_upload_tfstate (header included, any
2xx accepted); the direct-create path uploads the same way. Each
workflow ends up as state_uploaded or state_failed in the import result,
a workflow without its state fails the payload file and is retried on
the next import, and the checklist reports the SG-side upload next to
the TFC-side export.

Workaround for sg-cli; TODO(sg-cli) marks what to remove once its
upload sends the header and checks the status properly.
With more than one workflow to import, probe_import imports the first
selected workflow of the first payload file on its own and requires
both the create and the state upload to succeed before the rest is
imported in parallel. An environment problem (a store that rejects
uploads, a read-only token, a wrong connector kind) then costs one
workflow instead of all of them; the probe workflow is updated again
with its file afterwards.
…pdate path

sg-cli switches from create to update only when the API's message says
"Workflow name not unique", but the API answers 409 "Workflow ID not
unique", so every re-import of an existing workflow (a retry, --fresh,
the fail-fast probe) failed with that 409 and no state was uploaded.

import_bulk now treats a 409 / "not unique" create failure as an
existing workflow: it PATCHes the payload entry via sg_update_workflow,
uploads its state and drops the failure line, so re-runs are idempotent.
The direct-create path does the same fallback in sg_create_workflow.

Workaround for sg-cli, marked TODO(sg-cli).
…if needed

Customers cloned or forked the repo per release and had no way to pick up
fixes. ./sg-migrate.sh update runs on the host: it refuses a non-git
checkout, a detached HEAD or dirty tracked files, then fast-forwards the
current branch and rebuilds the Docker image only when the Dockerfile
changed. Config and output are untracked, so a pull never touches them.
A Git-for-Windows clone with core.autocrlf turns the bash scripts into
CRLF and they fail under WSL/Docker.
…rs and workflow group

Connectors and the other SGDefault* values could only be set globally or
per workspace. projectOverrides, keyed by the TFC project name, applies
to every workspace of a project; precedence is workspaceOverrides >
projectOverrides > SGDefault*. Its workflowGroup field picks the
StackGuardian workflow group for the whole project (default
tfc-<project>) and is written into every payload entry's
CLIConfiguration.WorkflowGroup.name, so the importer has one source of
truth; the payload file keeps its sg-payload.<project>.json name.

Overrides now resolve once per workspace into local.effective (each
layer null-filtered before merge()). Object-shaped fields are picked
with the tuple idiom already used for RunnerConstraints, since a partial
VCSTriggers or DeploymentPlatformConfig override does not unify with
the derived value in a conditional.

Both override maps are typed any with a field-name validation:
map(object({... = optional(any)})) rejects entries whose object-shaped
fields differ ("attribute types must all match for conversion to map"),
which already bit workspaceOverrides as soon as two workspaces set
different fields.

The summary gains projects (name -> segment, group, count),
workspaceProjects, workflowGroups and unknownProjectOverrides (keys that
match no TFC project, also flagged in migration-summary.md).
…oudAuthVars)

TFC workspaces carry ARM_*, AWS_ACCESS_KEY_ID, GOOGLE_CREDENTIALS and the
like; in StackGuardian the cloud connector provides these, and migrated
copies fight it. Only TFC_*/TFE_* were stripped so far.

stripCloudAuthVars (default true) drops the credential env variables of
the family matching each workflow's effective connector kind (AWS_* ->
AWS, AZURE_* -> AZURE, GCP_* -> GCP); cloudAuthVarPatterns holds the
regexes per family and can be replaced. Env variables only; terraform
inputs are untouched. A sensitive credential is stripped as well, so
the post-import step does not create a CHANGE_ME placeholder secret that
would shadow the connector. Stripped variables are listed in the summary
(strippedCloudAuthVars, with the connector kind) and the enrich step
applies the same rule to variable-set variables.
…s in the plan

The importer takes each project's workflow group from the payload
(CLIConfiguration.WorkflowGroup.name, written by the transformer from
projectOverrides.<project>.workflowGroup or tfc-<project>); the legacy
.sg/workflow-groups.json still applies on top, with a deprecation
warning. An existing group is reused (STATUS reuse), a missing one is
created unless --no-create-groups (missing!). The old rule that
overridden groups must pre-exist is gone.

StackGuardian cannot move workflows between groups, so the plan refuses
a project whose workflows still live in the group recorded in
.sg/state.json (or the default tfc-<project> group) when the target
changed (STATUS moved!), and two projects mapped to one group with
overlapping workflow names. Problems are listed as ✗ lines, the full
plan is still shown (also with --dry-run), and the run stops before the
confirmation.

The per-workflow ACTION column now says skip for a file that
state_import_done will skip, next to update and create. List endpoints
are read through a paginated helper (the API pages at 50), and the
migration summary shows the group each project maps to plus the new
strippedCloudAuthVars / unknownProjectOverrides sections.
The trigger endpoint upserts (a second POST answers 200 "VCS triggers
updated"), so re-runs were already safe but re-posted every trigger.
do_set_triggers keeps the sha of each posted body in state
(triggers.<seg>.sha) and skips a workflow whose triggers are unchanged
and already set; --fresh or an edited trigger re-posts. The call goes
through sg_set_vcs_triggers, which also treats a 4xx "already exists"
answer from older builds as success and reports other errors with the
API hint table.

Secret stubs were already reused, never overwritten; the log line and
the checklist now say so.
With more than one TFC project selected, the wizard's new step 3 asks
whether the global connectors and the tfc-<project> groups apply to all
projects. If not, it asks per project for the cloud connector, the VCS
connector (kind and repo prefix follow, with the usual mismatch warning)
and the workflow group — the default tfc-<project>, one of the org's
existing groups (GET wfgrps/listall/) or a typed name — and writes the
answers as projectOverrides. "same as the default" removes the key so
the global value flows through; fields the wizard does not manage
(Approvers, RunnerConstraints, ...) are kept. A re-run with existing
entries defaults to reviewing them and pre-selects the previous picks.

terraform.tfvars is rendered wholesale from the wizard's answers, so a
hand-written workspaceOverrides (or projectOverrides) block used to be
deleted by the next init. Both maps are now read before the write and
re-rendered through a JSON-shaped HCL map renderer (_tfvars_map) that
hcl2json round-trips; inner comments are the only thing not preserved.
The policy step also asks about stripCloudAuthVars, and the review shows
one row per project plus what was kept from the file.
Connector ids, private runner groups and DeploymentPlatformConfig kinds
inside projectOverrides are checked like the global and per-workspace
ones; a project's VCS connector is compared with the VCS kind that
project ends up with; projectOverrides keys that match no TFC project
are flagged as a typo (the real project names are listed). The import
context states the workflow-group policy (reuse when it exists, create
otherwise, or must-exist with --no-create-groups) and warns early when a
project's group changed since its last import, which the plan would
refuse as a move. One line reports whether cloud credential variables
are stripped.
…update semantics

README: how init picks connectors and the group per project, the
projectOverrides precedence, that hand-written override blocks survive
init, the workflow-group rules (reuse/create, never moved, no name
collisions across projects sharing a group, mapping file deprecated),
the cloud credential variables that are stripped per connector kind,
and what a re-run updates (payload change -> update, unchanged -> skip,
triggers only when changed, secrets never overwritten). CLAUDE.md
follows the code: plan_groups, group_for, the trigger sha, _tfvars_map,
wizard_projects, the preflight collectors and the state layout.
… workspace

After the real projectOverrides / workspaceOverrides maps, terraform.tfvars
now carries one commented entry for every selected project and workspace
that has no entry yet, pre-filled with what it gets today: the picked
connectors, runners and approvers (a workspace inherits its project's
override when one exists), the default tfc-<project> group, and for a
workspace the Terraform version it runs in TFC. Fine-tuning becomes
"move the entry up and change a value" instead of typing field names;
each entry is annotated with its workspace count or project and version.
_tfvars_map aligns attribute names and takes per-key notes.
apply already passed the patterns to terraform, but the plan, the import
subset, the probe, the triggers and the secret stubs compared exact names,
so 'import --workspace team-*' imported nothing. One matcher (bash case /
jq ws_selected) now serves all of them; '*' alone means no filter so the
unchanged-file skip still applies, and a filter matching nothing is an
error instead of an empty success.
…orkspace)

The provider only excludes by tag, so the module filters the selected
workspaces itself (local.selectedWorkspaces) and everything downstream,
state export included, follows. terraform.tfvars keeps the permanent list,
--exclude-workspace adds to it for one run, and the run scope helpers move
to lib/scope.sh so apply, preflight, the plan, the import subset, the probe
and the triggers use one definition. Excluded workspaces are listed in the
migration summary; init keeps the list on a re-run and the review shows it.
Until now --project only picked payload files after a full export and had
to be the file segment. The module gets tfProjects (names or slugs, [] =
all), applied after the name filters: tfe_workspace is read for the
name-filtered set because the project of a workspace is only known from
there. The orchestrator passes --project as tfProjects, matches payload
files by slug so 'My First Project' and my-first-project are the same, and
preflight refuses a --project that names no project (a tfProjects typo is
a warning) and previews the count the apply will export.
--tag replaces tfWorkspaceTags, --exclude-tag adds to tfWorkspaceIgnoreTags,
both are handed to terraform apply and previewed by preflight. Tags are not
in the payload, so they shape the export only.
The orchestrator read SG_TFVARS but terraform apply still loaded
terraform.tfvars from the module dir, the enrich script fell back to the
default path and the Docker wrapper did not forward the variable. The path
is now resolved once (absolute), passed to -var-file and to enrich, and the
wrapper translates a host path into the container: under /app when the file
is in the checkout, mounted read-only otherwise. clean --all only removes
the module's own file, and apply warns when both files exist because
terraform auto-loads terraform.tfvars on top of -var-file.
… set -u

sg_create_workflow tested SG_HTTP_CODE after a $(...) call that never sets
it in the calling shell, so with set -u the 409 fallback died before the
PATCH and the workflow was reported as failed with an empty message. The
'<code>: <body>' text the helper prints already carries the status.
import (and all) write export/run-result.json and run-summary.md: outcome
(planned, blocked, success, failed), the scope flags, the group table, the
plan problems and one row per workflow with plan, result, Terraform
version, state and trigger status; the markdown is made for a CI job
summary. Import results and state gain an 'updated' list so a PATCHed
workflow is told from a created one (the probe workflow counts as created).
--dry-run is documented for 'all' too: the local export runs, nothing is
created in StackGuardian.
…uns keep unmanaged keys

A file written by an older version keeps working (missing settings take
their defaults), but users had no way to see what is new short of diffing
the example. init --upgrade appends every setting the file lacks with the
comment and default from terraform.tfvars.example under a dated header and
touches nothing else, so hand comments survive and it runs without a
terminal; preflight names the missing settings until then. The wizard
re-run now also carries over settings it does not ask about (tfProjects,
cloudAuthVarPatterns, hand-added keys) instead of dropping them.
…e's uncommented ones

The first version derived the list from terraform.tfvars.example, so the
override maps, cloudAuthVarPatterns and tfHostname were never appended. The
list now comes from variables.tf; a setting the example only shows commented
out is appended that way (plus 'key = <default>' when the default fits on
one line), a mention in the file counts as present so a second run adds
nothing, the result is parsed and rolled back if broken, and init writes
tfHostname so a fresh file is complete.
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.

1 participant