Skip to content

TOPS-2851 - add scripts/migrate_v1.py (envars v1 to envars2) - #22

Merged
kthhrv merged 5 commits into
masterfrom
kh/tops-2851-migrate-v1
Oct 3, 2026
Merged

kthhrv merged 5 commits into
masterfrom
kh/tops-2851-migrate-v1

Conversation

@kthhrv

@kthhrv kthhrv commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

JIRA Ticket: TOPS-2851

Summary

This PR adds scripts/migrate_v1.py. It converts an envars v1 envars.yml to envars2 and encrypts the secrets again with a GCP KMS key. It then checks that v1 and v2 give exactly the same values.

TOPS-2851 moves every live data-app DAG from envars v1 (AWS KMS) to envars2 (tog-data-apps), in 29 draft PRs. The script made the envars.yml in those PRs. With this repo's version, all 29 files give exactly the same values as their v1 file on master, in every environment (one dev value in da-splash-access-dwelltime is an intended fix, documented in that PR). The script is in this repo so that the next migration uses the same tested script, not a copy on one laptop.

What the script does

  1. It decrypts the v1 secrets with envars v1, for the target environments only. This needs AWS credentials for the old AWS KMS key.
  2. It makes the envars2 file with envars init and envars add.
    • envars2 needs a secret to be scoped. So an inherited v1 secret default gets one encrypted value for each environment (and location) that has no override of its own, and each one uses that scope's own value. A top-level TOKEN: !secret … is handled the same way.
    • v2 locations are only added for the v1 accounts (master, sandbox) that the file uses. Data-apps use none.
    • {{ STAGE }} and {{ RELEASE }} become env.get(...), so that they are not circular. {{ RELEASE }} becomes env.get("RELEASE") or env.get("RELEASE_SHA"): v1 read RELEASE_SHA, and that input still works.
    • The most specific scope is written first (env+location, env, location, default), so that envars add's dependency check does not reject a default before its override exists.
  3. It runs envars validate. Then it compares the v1 values (envars print -y) with the v2 values (envars output --format json) for every environment and location. The comparison is exact: spaces at the start or end, multi-line values, and a missing key versus an explicit null. During the check only RELEASE_SHA is set, so it proves that the v1 input still works. If anything is different, it stops.

Safety

Risk What the script does
Secret value on the command line (ps, shell history) Secrets go through envars add --value-from-file with a mode-600 temp file. The file is deleted immediately
Secret value in the output Commands are printed as VAR=<hidden>. Decrypted output is never printed. A failed comparison prints key names only
Secret value in an error For commands that handle values, stderr is not printed (an error can quote part of a value). envars subprocesses also get _TYPER_STANDARD_TRACEBACK=1, so a traceback cannot show local variables
Bad or empty secret written A failed decrypt, or a secret with no value or an empty value, stops the script
Changed value not seen Exact comparison of structured output, not a text parser, with no exceptions. A missing key is a difference, also against a null
Wrong key --kms-key is required. There is no default, because the key controls who can decrypt later
KMS SERVICE_DISABLED from the ADC quota project Optional --quota-project, for envars subprocesses only. The user's gcloud and ADC config do not change

Things the user must do after the file (in the docs, and printed by the script when they apply)

  • {{ STAGE }} becomes env.get("ENVARS_ENV"). The CLI sets ENVARS_ENV, but get_env() does not, so set os.environ["ENVARS_ENV"] = env before you call it.
  • If the v2 file has locations, they keep the AWS account IDs. With a GCP KMS key, get_env() cannot find the location by itself, so pass loc=.

Files

  • scripts/migrate_v1.py: the script.
  • tests/test_migrate_v1.py: 24 test cases, for example:
    • secrets go by file, not argv (mode 600, deleted);
    • hidden stderr;
    • missing or empty secrets stop the script;
    • top-level scalar secrets;
    • per-scope secret defaults, including a location default and a partial location override;
    • --environments (decryption scope, and spaces);
    • exact comparison (a padded value fails if it changes) with no RELEASE exception;
    • both RELEASE forms, and the rendered RELEASE value with the real envars2 CLI for RELEASE_SHA only, RELEASE only, and neither;
    • overrides are written before defaults; a missing key versus a null;
    • an end-to-end migration with the real envars2 CLI and a small fake envars v1, which includes a multi-line value and a padded value.
  • docs/user-guide/07-migrating-from-v1.md, and mkdocs.yml nav: how to use the script.
  • docs/project/changelog.md: an "Unreleased" entry.

Tests

  • uv run pytest: 148 passed. They also pass with CI's relative dummy credentials path (the first CI run failed because the end-to-end test changed directory; fixed).
  • uv run pre-commit run on the changed files: ruff format, ruff check (with bandit) and ty all pass. mkdocs build --strict passes.
  • A real run of the current script (fa0aee7) against a copy of the v1 envars.yml from da-sighore-to-datalake, in a scratch directory:
    • 6 secret values were decrypted with AWS KMS and encrypted again with tog-data-apps;
    • the v1 and v2 values matched exactly for dev, prod and staging;
    • no values were printed.
  • The exact comparison from this script, run against the envars.yml in all 29 TOPS-2851 data-app PRs: all match their v1 file on master (dwelltime: one intended dev change, see that PR).
  • AWS CodeBuild: see the check on the latest commit.

Review

Four rounds of automated review (Gemini once, Copilot three times), 19 threads, plus 3 findings in one Copilot review summary. All are fixed or answered, and all threads are resolved.

After merge

Nothing deploys. This PR adds a script, its tests and its docs. The library and the CLI do not change.

Rollback

Revert this PR.

The script converts an envars v1 envars.yml to envars2 and encrypts its
secrets again with a GCP KMS key. It checks that v1 and v2 give the same
output for every environment and location. It never prints secret values
and never puts them on the command line. Includes tests and a user-guide page.

JIRA Ticket: TOPS-2851
Copilot AI balanced review requested due to automatic review settings October 2, 2026 22:59

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a migration script scripts/migrate_v1.py along with documentation and tests to help users transition from envars v1 to envars2. The script handles re-encrypting secrets using a GCP KMS key and validates that the outputs match. The review feedback highlights three important issues: a critical bug where top-level secrets are skipped during migration, a high-severity issue where multi-line secrets are corrupted by the line-based parser, and a potential IndexError if target_envs is empty.

Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved secret exposure, value preservation, and fallback-scoping issues undermine migration safety.

Review effort: Balanced
Findings: 4 High severity · 4 Medium severity · 1 Low severity

Open (9)
What changed in this PR

Adds a reusable envars v1 migration script without changing the library or CLI.

Changes:

  • Converts configuration and re-encrypts secrets with GCP KMS.
  • Validates output and compares resolved values.
  • Adds migration tests and documentation.
File Description
tests/​test_migrate_v1.py Tests migration behavior and secret handling.
scripts/​migrate_v1.py Implements conversion, encryption, and verification.
mkdocs.yml Adds migration guide navigation.
docs/​user-guide/​07-migrating-from-v1.md Documents migration and application updates.
docs/​project/​changelog.md Records the migration tooling addition.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py
Comment thread scripts/migrate_v1.py Outdated
Comment thread docs/user-guide/07-migrating-from-v1.md
- Read v1 with `print -y` and v2 with `output --format json`, so that
  verification compares exact values (spaces at the start or end,
  multi-line values).
- Migrate top-level scalar secrets.
- Write inherited secret defaults for each environment and location with
  that scope's own value.
- Decrypt only --environments. Reject empty secrets and an empty
  environment list.
- Hide stderr for commands that handle values.
- Note the ENVARS_ENV step for get_env() when {{ STAGE }} is rewritten.
- Fix the end-to-end test for CI's relative credentials path.

JIRA Ticket: TOPS-2851
Copilot AI balanced review requested due to automatic review settings October 3, 2026 08:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py
Comment thread docs/user-guide/07-migrating-from-v1.md Outdated
- Rewrite both RELEASE forms when a value has both.
- Remove the RELEASE exception in verification. Give v1 (RELEASE_SHA)
  and v2 (RELEASE) the same value during verification.
- Strip spaces in --environments.
- Note that get_env() needs loc= when the v2 file has locations.
- Docs: give the Git requirement line (the package name is envars).

JIRA Ticket: TOPS-2851
Copilot AI balanced review requested due to automatic review settings October 3, 2026 09:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Unresolved migration and verification issues can reject valid configurations or overlook runtime differences.

Review effort: Balanced
Findings: None

Resolved since last review (5)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Preserve RELEASE_SHA runtime lookup during migration

scripts/​migrate_v1.py:157

These substitutions change the runtime source from RELEASE_SHA to RELEASE, as the comment in verify_migration confirms. A deployment that still sets only RELEASE_SHA gets None or not-set after migration. Verification masks that difference by setting both variables; matching those synthetic inputs does not preserve existing deployment behavior. Preserve the legacy lookup, update the substitution tests, and cover a runtime environment containing only RELEASE_SHA.

Medium severity Write overrides before defaults to avoid false dependency cycles

scripts/​migrate_v1.py:231

Writing defaults before overrides can reject an otherwise valid migration when locations are configured. For example, with only prod and location master, A.default = '{{ B }}', B.default = '{{ A }}', and B.prod = 'fixed' are acyclic once resolved. Adding B's default temporarily creates a cycle, and envars add exits during its per-context dependency check (src/envars/cli.py:387–392) before the override is written. The same problem affects location defaults. Collect additions and write higher-specificity overrides before fallbacks, or assemble the complete configuration before checking dependencies.

Medium severity Distinguish missing keys from explicit null values

scripts/​migrate_v1.py:268

A missing key compares equal to an explicit null because .get() returns None for both: compare({'OPTIONAL': None}, {}) reports no differences. Both readers preserve nulls, so verification can report an exact match after a null-valued variable disappears. Require the key to exist in both mappings before accepting equal values.

…pare

- Collect every value, then write the most specific scope first, so that
  envars add's dependency check does not reject a default that is only
  circular until its override exists.
- compare: a key that is missing on one side is a difference, even when
  the other side has an explicit null.

JIRA Ticket: TOPS-2851

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Null-key comparison and changed release-variable inputs can undermine migration equivalence.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)

Comment thread scripts/migrate_v1.py Outdated
Comment thread scripts/migrate_v1.py Outdated
v1 rendered {{ RELEASE }} from RELEASE_SHA. The rewrite is now
env.get("RELEASE") or env.get("RELEASE_SHA", ...), so the v1 input still
works and RELEASE also works. Verification sets only RELEASE_SHA, so it
proves the v1 input. A test renders the template with the real envars2
CLI for RELEASE_SHA only, RELEASE only, and neither.

JIRA Ticket: TOPS-2851
Copilot AI balanced review requested due to automatic review settings October 3, 2026 10:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Secret file transfer normalizes carriage returns, changing affected secrets and failing exact verification.

Review effort: Balanced
Findings: None

Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Secret file reading normalizes CRLF and carriage returns

scripts/​migrate_v1.py:198

The secret file handoff changes CRLF and bare carriage returns to LF: envars add reads this file with open(value_from_file) at src/envars/cli.py:238–239, which normalizes newlines before encryption. Secrets containing these characters are therefore changed, and the exact verification fails. Use open(value_from_file, newline="") in the CLI reader and add a secret round-trip regression test covering both forms.

@kthhrv
kthhrv merged commit c665985 into master Oct 3, 2026
2 checks passed
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.

2 participants