From 4b81f6b9be7eb160c245198ab03e83d82ec170af Mon Sep 17 00:00:00 2001 From: akaur-ld Date: Fri, 27 Feb 2026 16:07:55 -0500 Subject: [PATCH 1/2] logging and include flags --- README.md | 98 +++++ docs/migration-guide.md | 363 ++++++++++++++++++ examples/revert-migration-example.yaml | 10 +- .../workflow-extract-migrate-incremental.yaml | 51 ++- examples/workflow-full.yaml | 94 +++-- examples/workflow-revert-and-migrate.yaml | 94 +++-- .../migrate_between_ld_instances.ts | 90 ++++- .../revert_migration.ts | 73 ++-- .../launchdarkly-migrations/workflow.ts | 2 + 9 files changed, 775 insertions(+), 100 deletions(-) create mode 100644 docs/migration-guide.md diff --git a/README.md b/README.md index d65b437..ce79925 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ root/ │ └── reports/ # Import operation reports ``` +## Documentation + +- **[Migration guide (extract, migrate, revert)](docs/migration-guide.md)** – Step-by-step walkthrough of the export, migrate, and revert process, with include flags, parameters, and YAML reference. + ## Multi-Region and Custom Instance Support All scripts support configurable LaunchDarkly domains to work with different instances and regions: @@ -144,6 +148,8 @@ deno task workflow -f my-migration.yaml 2. Map members between instances 3. Migrate to destination +To sync **only changes** after an initial migration, see [Incremental Sync (Step-by-Step)](#incremental-sync-step-by-step). + ### Option 2: Individual Steps Or run migration steps individually: @@ -209,6 +215,7 @@ The workflow orchestrator allows you to run complete end-to-end migrations from - ✅ **Full workflow automation** - Extract → Map → Migrate in one command - ✅ **Selective step execution** - Run only the steps you need +- ✅ **Incremental sync** - After an initial full migration, re-run extract + migrate to sync only changed flags, environments, and segments (see [Incremental Sync (Step-by-Step)](#incremental-sync-step-by-step)) - ✅ **Third-party imports** - Import flags from external sources - ✅ **Default behavior** - Runs full workflow if no steps specified @@ -325,6 +332,90 @@ migration: dev: development ``` +### Incremental Sync (Step-by-Step) + +Incremental sync lets you run a full migration once, then later sync **only** the flags, environments, and segments that changed in the source project. Use `workflow-extract-migrate-incremental.yaml` (extract + migrate, no member mapping). + +#### Prerequisites + +- [Deno](https://deno.land/) 2.0+ installed. +- API keys in `config/api_keys.json` (source and destination). +- Source and destination LaunchDarkly projects (and optionally matching member mapping from a full workflow). + +#### Step 1: Initial export (full sync) + +Run the full workflow **once** to create the baseline and the sync manifest. The manifest is written to `./data/launchdarkly-migrations/` and is used later to detect what changed. + +1. Edit `examples/workflow-full.yaml` with your `source.projectKey`, `destination.projectKey`, and `domain` (if not default). +2. Run: + + ```bash + deno task workflow -f examples/workflow-full.yaml + ``` + + This runs **extract-source** → **map-members** → **migrate**. The migrate step performs a full migration and writes the sync manifest. You will not see per-flag **UPDATES** blocks during this run (those appear only during incremental sync). + +#### Step 2: Make changes in the source project + +In the LaunchDarkly UI (or via API), change the source project: edit a flag (on/off, rules, targeting), add a flag, or change segments/environments. Only those changes will be synced in the next step. + +#### Step 3: Sync incrementals + +Re-extract the source and migrate only what changed. + +1. Edit `examples/workflow-extract-migrate-incremental.yaml` with the **same** `source` and `destination` (and domains) as in Step 1. +2. Run: + + ```bash + deno task workflow -f examples/workflow-extract-migrate-incremental.yaml + ``` + + This runs **extract-source** (overwrites the previous extract with current source data) then **migrate** with `incremental: true`. The migrate step compares against the sync manifest and updates only flags, environments, and segments that changed since the last run. + +#### What you'll see + +- **Progress:** `[N/M] Processing flag: ` for each flag. +- **Unchanged:** Lines like `✓ : unchanged (v...), skipping` and `✓ : unchanged (v...), skipping` for resources that were not modified. +- **Changed (incremental runs only):** For each updated flag/environment, a detailed **UPDATES** block: + - `UPDATES` + - `Flag Key: ` + - `Environment: ` + - `Keys: archived, contextTargets, fallthrough, ...` + These blocks appear **only** when you run the migrate step with `--incremental` (e.g. via `workflow-extract-migrate-incremental.yaml`); they are not shown during the initial full export. +- **End of run:** A **MIGRATION SUMMARY** (including whether conflicts were encountered) and a **MIGRATION RESULT** box with flags created, flags updated, flags total, and total time. The line *"More detailed changes per flag are logged above"* refers to the UPDATES blocks when running incrementally. + +#### Config reference: Extract + Migrate (Incremental) + +```yaml +# workflow-extract-migrate-incremental.yaml +workflow: + steps: + - extract-source + - migrate + +source: + projectKey: my-source-project + domain: app.launchdarkly.com + +destination: + projectKey: my-dest-project + domain: app.launchdarkly.com + +extraction: + includeSegments: true + +migration: + migrateSegments: true + assignMaintainerIds: false + incremental: true # First run: full sync + create manifest; later runs: sync only changes +``` + +```bash +deno task workflow -f examples/workflow-extract-migrate-incremental.yaml +``` + +**Optional:** Set `migration.dryRun: true` in the YAML to preview what would be synced without applying changes to the destination. + ### Workflow Configuration Structure ```yaml @@ -358,6 +449,7 @@ migration: # Optional - for migrate step environmentMapping: sourceEnv: destEnv dryRun: boolean # Preview changes without applying + incremental: boolean # Skip unchanged flags/envs/segments (version-based); see Incremental Sync thirdPartyImport: # Required for third-party-import step inputFile: string # JSON or CSV file path @@ -407,6 +499,7 @@ By default, segments are **not** extracted unless explicitly needed, preventing - `examples/workflow-full.yaml` - Complete workflow (default behavior) - `examples/workflow-extract-only.yaml` - Extract source data only +- `examples/workflow-extract-migrate-incremental.yaml` - Extract + migrate with incremental sync (only changed flags/envs/segments on subsequent runs) - `examples/workflow-migrate-only.yaml` - Migrate with pre-extracted data - `examples/workflow-third-party.yaml` - Third-party flag import - `examples/workflow-custom-steps.yaml` - Custom step combinations @@ -548,6 +641,7 @@ deno task migrate -p SOURCE_PROJECT -d DEST_PROJECT -v my-target-view **Notes:** - Views are an Early Access feature in LaunchDarkly - If a view already exists in the destination, it will be reused +- **You can create the intended view in the destination project before migrating** (e.g. in the LaunchDarkly UI); the script will detect it and link flags to it without creating a duplicate - View associations are preserved from the source project - The `-v` flag adds an additional view linkage (doesn't replace existing ones) @@ -960,6 +1054,10 @@ don't need to specify them manually. - `-v, --targetView`: (Optional) View key to link all migrated flags to. This will link all flags to the specified view in addition to any views they were already linked to in the source project. +- `--include-flags`: (Optional) Comma-separated list of flag keys to migrate + (e.g., 'my-flag,other-flag'). If not specified, all flags from the extracted + source are migrated. Only flag keys that exist in the extracted data are used; + others are skipped with a warning. - `-e, --environments`: (Optional) Comma-separated list of environment keys to migrate (e.g., 'production,staging'). If not specified, all environments will be migrated. Useful for selective environment migration or testing. diff --git a/docs/migration-guide.md b/docs/migration-guide.md new file mode 100644 index 0000000..34f6592 --- /dev/null +++ b/docs/migration-guide.md @@ -0,0 +1,363 @@ +# LaunchDarkly Migration Guide: Extract, Migrate & Revert + +This guide walks through the full process of **exporting** (extracting) data from a source LaunchDarkly project, **migrating** it to a destination project, and **reverting** a migration when needed. It also lists all YAML parameters you can change and when to use them. + +--- + +## Table of contents + +1. [Overview](#overview) +2. [Prerequisites](#prerequisites) +3. [Parameters to modify per YAML (before you run)](#parameters-to-modify-per-yaml-before-you-run) +4. [Phase 1: Extract (export)](#phase-1-extract-export) +5. [Phase 2: Migrate](#phase-2-migrate) +6. [Phase 3: Revert](#phase-3-revert) +7. [Include flags and other options](#include-flags-and-other-options) +8. [YAML parameters reference](#yaml-parameters-reference) +9. [Quick reference: which file to use](#quick-reference-which-file-to-use) + +--- + +## Overview + +The migration flow has three main steps: + +| Step | What it does | +|------|----------------| +| **Extract (export)** | Reads flags, environments, and optionally segments from the **source** project and saves them under `data/launchdarkly-migrations/source/`. | +| **Map members** | (Optional) Builds a mapping of source account members to destination account members so migrated flags can keep maintainer assignments. | +| **Migrate** | Creates (or updates) flags, environments, and segments in the **destination** project using the extracted data. | + +**Revert** undoes a migration: it finds the migrated flags in the destination (using extracted source data), archives and deletes them, and optionally cleans up views. + +You can run these steps via: + +- **Workflow** (recommended): one command runs multiple steps from a single YAML config. +- **Individual tasks**: run extract, map-members, or migrate separately with their own config/CLI args. + +--- + +## Prerequisites + +- **Deno** installed. +- **API access**: destination project API token (and source token if extract and destination are in different accounts). Configure in `config/api_keys.json` or via environment (see README). + +--- + +## Parameters to modify per YAML (before you run) + +For each workflow or config file, change these parameters **before** running the step. Required items must be set; optional items depend on your setup. + +### `examples/workflow-full.yaml` (full migration: extract → map-members → migrate) + +| Parameter | What to set | +|-----------|-------------| +| `source.projectKey` | Your source LaunchDarkly project key. | +| `source.domain` | e.g. `app.launchdarkly.com` or `app.eu.launchdarkly.com`. | +| `destination.projectKey` | Your destination project key. | +| `destination.domain` | Destination LaunchDarkly domain. | +| `memberMapping.outputFile` | Path for member mapping file; required if `migration.assignMaintainerIds: true`. | +| `migration.assignMaintainerIds` | `true` only if you want maintainer mapping (map-members step runs first). | +| `migration.migrateSegments` | `true` only if you set `extraction.includeSegments: true`. | +| `migration.includeFlags` | List of flag keys to migrate; omit or comment out to migrate all. | +| `migration.environments` | List of env keys to migrate; omit to migrate all. | +| `migration.environmentMapping` | Only if source and destination use different env names. | +| `extraction.includeSegments` | `true` if you will migrate segments (uncomment the block). | + +### `examples/workflow-extract-migrate-incremental.yaml` (extract + migrate, incremental sync) + +| Parameter | What to set | +|-----------|-------------| +| `source.projectKey` | Your source LaunchDarkly project key. | +| `source.domain` | e.g. `app.launchdarkly.com`. | +| `destination.projectKey` | Your destination project key. | +| `destination.domain` | Destination domain. | +| `extraction.includeSegments` | `true` only if `migration.migrateSegments: true`. | +| `migration.migrateSegments` | `true` if you want segments migrated. | +| `migration.assignMaintainerIds` | Usually `false` (no map-members step). | +| `migration.incremental` | Keep `true` for incremental sync. | +| `migration.includeFlags` | List of flag keys to migrate; omit to migrate all. | +| Do not set `migration.conflictPrefix` | Would create new flags each run instead of updating. | + +### `examples/workflow-migrate-only.yaml` (migrate only; extract already done) + +| Parameter | What to set | +|-----------|-------------| +| `source.projectKey` | Source project key (must match the project you extracted). | +| `destination.projectKey` | Destination project key. | +| `source.domain` / `destination.domain` | Domains if not default. | +| `migration.assignMaintainerIds` | `true` only if you have run map-members and have a mapping file. | +| `migration.migrateSegments` | `true` only if segments were extracted. | +| `migration.includeFlags` | List of flag keys; omit to migrate all. | +| `migration.environments` | List of env keys; omit to migrate all. | +| `migration.environmentMapping` | Only if env keys differ between source and destination. | +| `migration.conflictPrefix` | Only for initial full migration when keys collide; do not use with incremental. | +| `migration.targetView` | View to link migrated flags to. | + +### `examples/revert-migration-example.yaml` (revert only) + +| Parameter | What to set | +|-----------|-------------| +| `source.projectKey` | Source project key (same as the migration you are reverting). | +| `destination.projectKey` | Destination project key (flags will be deleted here). | +| `source.domain` / `destination.domain` | Domains if not default. | +| `migration.targetView` | View key used during migration; uncomment if you used a target view. | +| CLI `--dry-run` | Use for preview; omit to execute revert. | + +### `examples/workflow-revert-and-migrate.yaml` (revert then re-migrate) + +| Parameter | What to set | +|-----------|-------------| +| `source.projectKey` | Same as the migration you are reverting. | +| `destination.projectKey` | Destination project (revert deletes flags here; migrate runs here). | +| `source.domain` / `destination.domain` | Domains if not default. | +| `revert.dryRun` | `true` to preview revert; set `false` to execute. | +| `migration.dryRun` | `true` to preview migrate; set `false` to execute. | +| `migration.includeFlags` | List of flag keys to re-migrate; omit to migrate all. | +| Other `migration.*` | Same as other migrate workflows (environments, targetView, etc.). | +| Do not set `migration.conflictPrefix` | If you use incremental or want to update existing flags. | + +--- + +## Phase 1: Extract (export) + +Extract reads the source project and writes flag and project data to disk. This data is used by both migrate and revert. + +### What gets extracted + +- Project metadata, environments, flag list, and each flag’s full configuration (rules, targets, etc.). +- Optionally: segments (if `extraction.includeSegments: true` and you plan to migrate segments). + +### Commands + +**Option A – Full workflow (extract + map-members + migrate)** +Uses `workflow-full.yaml`. Extract runs as the first step. + +```bash +deno task workflow -f examples/workflow-full.yaml +``` + +**Option B – Extract + migrate only (no member mapping)** +Uses `workflow-extract-migrate-incremental.yaml`. Good for incremental syncs. + +```bash +deno task workflow -f examples/workflow-extract-migrate-incremental.yaml +``` + +**Option C – Extract only** +Use a workflow that only runs `extract-source`, or run the extract script directly (see README). + +See [Parameters to modify per YAML](#parameters-to-modify-per-yaml-before-you-run) for extract parameters by workflow file. + +--- + +## Phase 2: Migrate + +Migrate reads the extracted data and creates (or updates) flags and optionally segments in the destination project. + +### What migrate does + +- Creates flags in the destination (or updates them if they already exist and you’re not using a conflict prefix). +- **Important:** Do **not** use `conflictPrefix` with incremental sync. With a prefix set, the script creates new prefixed flags instead of updating existing ones, so incremental runs would keep creating duplicates. +- Copies environments, rules, targets, and other flag settings. +- Can limit which flags and which environments are migrated (see [Include flags and other options](#include-flags-and-other-options)). +- Optionally links flags to a view, maps maintainers, and maps environment keys. + +### Commands + +**Full workflow (extract → map-members → migrate):** + +```bash +deno task workflow -f examples/workflow-full.yaml +``` + +**Extract + migrate (incremental-friendly):** + +```bash +deno task workflow -f examples/workflow-extract-migrate-incremental.yaml +``` + +**Migrate only** (assumes extract already done): + +```bash +deno task workflow -f examples/workflow-migrate-only.yaml +``` + +Use **dry run first** when trying a new config: set `migration.dryRun: true` to preview, then set `dryRun: false` and run again to apply. See [Parameters to modify per YAML](#parameters-to-modify-per-yaml-before-you-run) for the full list of migration parameters. + +--- + +## Phase 3: Revert + +Revert removes migrated flags from the destination. It uses the same extracted source data to know which flags to remove (smart mode). + +### What revert does + +1. Identifies flags to revert (from source data or from flags in a target view). +2. Deletes pending approval requests for those flags. +3. Unlinks flags from the target view(s). +4. Archives and deletes the flags in the destination. +5. Optionally deletes the view(s). + +### Commands + +**Revert only (standalone):** + +```bash +# Preview +deno task revert -f examples/revert-migration-example.yaml --dry-run + +# Execute +deno task revert -f examples/revert-migration-example.yaml +``` + +**Revert then re-migrate (workflow):** + +```bash +deno task workflow -f examples/workflow-revert-and-migrate.yaml +``` + +With the workflow, set `revert.dryRun` and `migration.dryRun` to `true` for a preview, then to `false` to execute. + +### Parameters to set before revert + +In the revert config (e.g. `revert-migration-example.yaml` or `workflow-revert-and-migrate.yaml`): + +| Parameter | Where | Description | +|-----------|--------|-------------| +| `source.projectKey` | `source:` | Source project key (used to find which flags were migrated). **Required.** | +| `destination.projectKey` | `destination:` | Destination project (flags are deleted here). **Required.** | +| `source.domain` / `destination.domain` | `source:` / `destination:` | Domains. Optional. | +| `migration.targetView` | `migration:` | View key that was used during migration; revert will unlink flags from this view. Optional if using source-data smart mode. | +| `revert.dryRun` | `revert:` | `true` = preview only; `false` = execute. | +| `revert.deleteViews` | `revert:` | Set `true` to delete the target view(s) after unlinking. Optional. | +| `revert.viewKeys` | `revert:` | List of view keys to process. Optional; overrides using `migration.targetView`. | + +Revert works best when the same source data used for migration is still present (same `source.projectKey` and extracted data). It then knows exactly which flags to remove. + +--- + +## Include flags and other options + +### Migrating only specific flags + +Use `migration.includeFlags` in any workflow that runs the migrate step: + +```yaml +migration: + includeFlags: + - config-welcome-message + - my-feature-flag +``` + +- Only these flag keys are migrated (and they must exist in the extracted source). +- **Comment out or remove `includeFlags`** to migrate all extracted flags. + +### Other useful migration options + +- **Environments:** Migrate only certain environments: + ```yaml + migration: + environments: + - production + - staging + ``` + +- **Environment mapping:** Source and destination use different env names: + ```yaml + migration: + environmentMapping: + prod: production + stg: staging + ``` + +- **Conflict prefix:** When the same flag key exists in both projects, create a prefixed copy in destination. **Do not use with incremental sync**—it would create new prefixed flags instead of updating existing ones. + ```yaml + migration: + conflictPrefix: "imported-" + ``` + +- **Target view:** Group migrated flags in a view (helps with revert and organization): + ```yaml + migration: + targetView: "migration-2024" + ``` + +- **Incremental / since:** In workflows that support it, use `migration.incremental: true` to sync only changes, or `migration.since: "2025-01-15"` to sync only flags modified after that date. + +--- + +## YAML parameters reference + +Parameters you can change in the YAML files, grouped by section. + +### Source and destination (all workflows) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `source.projectKey` | Yes | Source LaunchDarkly project key | `Aman-Migration-Test` | +| `source.domain` | No | Source LaunchDarkly domain | `app.launchdarkly.com` | +| `destination.projectKey` | Yes | Destination project key | `aman-migration-2` | +| `destination.domain` | No | Destination domain | `app.launchdarkly.com` | + +### Workflow steps (workflow files only) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `workflow.steps` | No | Steps to run (default: extract, map-members, migrate) | `["extract-source", "migrate"]` | + +### Extraction (when step extract-source is used) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `extraction.includeSegments` | No | Extract segments (needed if you migrate segments) | `true` | + +### Member mapping (when step map-members is used) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `memberMapping.outputFile` | No | Path for member ID mapping file | `data/.../maintainer_mapping.json` | + +### Migration (all workflows that run migrate) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `migration.assignMaintainerIds` | No | Use member mapping for maintainers | `true` / `false` | +| `migration.migrateSegments` | No | Migrate segments | `true` / `false` | +| `migration.conflictPrefix` | No | Prefix for conflicting flag keys. **Do not use with incremental sync.** | `"imported-"` | +| `migration.targetView` | No | View to link migrated flags to | `"migration-2024"` | +| `migration.includeFlags` | No | Only migrate these flag keys (omit = all) | `["flag-a", "flag-b"]` | +| `migration.environments` | No | Only migrate these environments (omit = all) | `["production", "staging"]` | +| `migration.environmentMapping` | No | Source → destination env key map | `{ "prod": "production" }` | +| `migration.dryRun` | No | Preview only | `true` / `false` | +| `migration.incremental` | No | Sync only changes (uses manifest) | `true` / `false` | +| `migration.since` | No | Only flags modified after date (ISO 8601) | `"2025-01-15"` | + +### Revert (revert workflow or revert config) + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| `revert.dryRun` | No | Preview only | `true` / `false` | +| `revert.deleteViews` | No | Delete view(s) after unlinking flags | `true` / `false` | +| `revert.viewKeys` | No | View keys to process | `["migration-2024"]` | +| `migration.targetView` | No | View used during migration (for revert) | `"migration-2024"` | + +--- + +## Quick reference: which file to use + +| Goal | YAML file | Command | +|------|-----------|---------| +| Full migration (extract + map members + migrate) | `examples/workflow-full.yaml` | `deno task workflow -f examples/workflow-full.yaml` | +| Extract + migrate, incremental sync | `examples/workflow-extract-migrate-incremental.yaml` | `deno task workflow -f examples/workflow-extract-migrate-incremental.yaml` | +| Migrate only (extract already done) | `examples/workflow-migrate-only.yaml` | `deno task workflow -f examples/workflow-migrate-only.yaml` | +| Revert only | `examples/revert-migration-example.yaml` | `deno task revert -f examples/revert-migration-example.yaml` | +| Revert then re-migrate | `examples/workflow-revert-and-migrate.yaml` | `deno task workflow -f examples/workflow-revert-and-migrate.yaml` | + +Before running, set at least: + +- **Any workflow:** `source.projectKey`, `destination.projectKey` (and optionally domains). +- **Migrate:** Any of `includeFlags`, `environments`, `environmentMapping`, `conflictPrefix`, `targetView`, `dryRun` as needed. +- **Revert:** Same source/destination as the migration; set `revert.dryRun` (and optionally `migration.targetView`, `revert.viewKeys`, `revert.deleteViews`). + +For more detail on CLI flags and advanced usage, see the main [README](../README.md). diff --git a/examples/revert-migration-example.yaml b/examples/revert-migration-example.yaml index 9e5cb6e..acc1881 100644 --- a/examples/revert-migration-example.yaml +++ b/examples/revert-migration-example.yaml @@ -13,16 +13,16 @@ # deno task revert -f examples/revert-migration-example.yaml # Execute the revert source: - projectKey: slack-app - domain: app.ld.catamorphic.com + projectKey: Aman-Migration-Test + domain: app.launchdarkly.com destination: - projectKey: default - domain: app.ld.catamorphic.com + projectKey: aman-migration-2 + domain: app.launchdarkly.com migration: # The view that was created during migration - targetView: "slack-app" + #targetView: "slack-app" # Note: When using --config/-f with the revert tool: # - source.projectKey: Used to identify which flags to revert diff --git a/examples/workflow-extract-migrate-incremental.yaml b/examples/workflow-extract-migrate-incremental.yaml index cb63c5d..effd3b2 100644 --- a/examples/workflow-extract-migrate-incremental.yaml +++ b/examples/workflow-extract-migrate-incremental.yaml @@ -1,24 +1,63 @@ -# Full sync: extract + migrate (no incremental) -# Run this FIRST to create the sync manifest. Then use workflow-nick-jordan-incremental.yaml -# to test incremental (re-extract + migrate with --incremental). +# ============================================================================= +# Extract + Migrate (Incremental Sync) +# ============================================================================= +# Runs: extract-source → migrate (no map-members). +# Use for incremental syncs: run once for full sync (creates manifest), then +# re-run after source changes to sync only what changed. Requires existing +# sync manifest from a previous run when incremental: true. +# +# Run: deno task workflow -f examples/workflow-extract-migrate-incremental.yaml +# +# When to modify: +# - Before first run: set source/destination, extraction, migration options. +# - For incremental: keep incremental: true and re-run after source changes. +# - For full sync: same; first run creates the manifest for future incremental. +# ============================================================================= workflow: steps: - extract-source - migrate +# ----------------------------------------------------------------------------- +# SOURCE & DESTINATION (required – modify before first run) +# ----------------------------------------------------------------------------- source: - projectKey: + projectKey: Aman-Migration-Test domain: app.launchdarkly.com destination: - projectKey: + projectKey: aman-migration-2 domain: app.launchdarkly.com +# ----------------------------------------------------------------------------- +# EXTRACTION (optional – modify if you need segments) +# ----------------------------------------------------------------------------- +# includeSegments: true only if migrateSegments: true below. +# extraction: includeSegments: true +# ----------------------------------------------------------------------------- +# MIGRATION (modify to control what and how to migrate) +# ----------------------------------------------------------------------------- +# incremental: true = skip flags/envs unchanged since last sync (uses manifest). +# Include flags: limit to specific flags; comment out to migrate all extracted. +# +# IMPORTANT: Do not set conflictPrefix when using incremental sync. With +# conflictPrefix on, the script would create new prefixed flags instead of +# updating existing ones, breaking incremental updates. +# migration: migrateSegments: true assignMaintainerIds: false - incremental: true # Full sync; creates sync manifest for incremental runs + + # Keep true for incremental sync. First run creates manifest; later runs sync only changes. + incremental: true + + # Do NOT use conflictPrefix with incremental: true (would create new flags each run). + # conflictPrefix: "us-" + + # Migrate only these flag keys. Comment out to migrate all extracted flags. + includeFlags: + - config-welcome-message diff --git a/examples/workflow-full.yaml b/examples/workflow-full.yaml index d173907..1c4be62 100644 --- a/examples/workflow-full.yaml +++ b/examples/workflow-full.yaml @@ -1,51 +1,95 @@ +# ============================================================================= # Complete LaunchDarkly Migration Workflow -# Runs all steps: extract source → map members → migrate -# This is the DEFAULT behavior if workflow.steps is not specified +# ============================================================================= +# Runs: extract source → map members → migrate +# Use this for a full migration (all three steps). Modify the sections below +# before running. Required: source.projectKey, destination.projectKey. +# +# Run: deno task workflow -f examples/workflow-full.yaml +# ============================================================================= -# Optional: Explicitly define steps (if omitted, runs all steps by default) +# ----------------------------------------------------------------------------- +# WORKFLOW STEPS (optional) +# ----------------------------------------------------------------------------- +# Override only if you want to skip steps. If omitted, runs: extract-source, +# map-members, migrate. Example: use ["extract-source", "migrate"] to skip +# member mapping. +# # workflow: # steps: # - extract-source # - map-members # - migrate +# ----------------------------------------------------------------------------- +# SOURCE & DESTINATION (required – modify before first run) +# ----------------------------------------------------------------------------- +# Source = project to read from. Destination = project to write to. +# domain: Use app.eu.launchdarkly.com for EU; otherwise app.launchdarkly.com. +# source: - projectKey: us-production - domain: app.launchdarkly.com # Optional: Defaults to app.launchdarkly.com + projectKey: Aman-Migration-Test + domain: app.launchdarkly.com destination: - projectKey: eu-production - domain: app.launchdarkly.com # Optional: Use app.eu.launchdarkly.com for EU instance + projectKey: aman-migration-2 + domain: app.launchdarkly.com -# Optional: Control what data to extract from source +# ----------------------------------------------------------------------------- +# EXTRACTION (optional – modify if you need segments) +# ----------------------------------------------------------------------------- +# Controls what is extracted in the extract-source step. +# includeSegments: true only if you will migrate segments (migrateSegments: true). +# Leave commented to use defaults (flags only). +# # extraction: -# includeSegments: true # Uncomment to extract segments (only needed if migrateSegments: true) +# includeSegments: true -# Optional: Configure member mapping output location +# ----------------------------------------------------------------------------- +# MEMBER MAPPING (optional – modify to change output path) +# ----------------------------------------------------------------------------- +# Used by the map-members step. Output file path for source → destination +# member ID mapping. Required if assignMaintainerIds: true in migration. +# memberMapping: outputFile: data/launchdarkly-migrations/mappings/maintainer_mapping.json -# Migration configuration +# ----------------------------------------------------------------------------- +# MIGRATION (modify to control what and how to migrate) +# ----------------------------------------------------------------------------- +# All options below apply to the migrate step. Change before run or between +# runs to narrow scope (e.g. includeFlags, environments) or fix conflicts. +# migration: - # Use the member mapping we just created + # Assign maintainers using the mapping from map-members. Set false to skip. assignMaintainerIds: true - - # Include segments + + # Migrate segments in addition to flags. Set false for flags-only. migrateSegments: true - - # Handle conflicts - conflictPrefix: "us-" - - # Organize in view - targetView: "us-migration-2024" - - # Only migrate these environments + + # If a flag/key already exists in destination, create with this prefix instead. + # Uncomment only for initial full migration when source and destination share keys. + # Do NOT use with incremental sync (would create new prefixed flags instead of updating). + # conflictPrefix: "us-" + + # Link all migrated flags to this view in destination. View created if missing. + # Uncomment to group migrated flags (e.g. for revert or organization). + # targetView: "us-migration-2024" + + # Migrate only these flag keys. Comment out or remove to migrate all extracted flags. + # Modify when testing or migrating a subset. + includeFlags: + - config-welcome-message + + # Migrate only these environments. Omit or comment out to migrate all environments. environments: - production - staging - - # Map environment keys if needed + - test + + # Map source environment keys to different destination keys. + # Use when source and destination use different env names (e.g. prod vs production). environmentMapping: production: production staging: staging - + test: test diff --git a/examples/workflow-revert-and-migrate.yaml b/examples/workflow-revert-and-migrate.yaml index bb96e72..5e1e07d 100644 --- a/examples/workflow-revert-and-migrate.yaml +++ b/examples/workflow-revert-and-migrate.yaml @@ -1,62 +1,94 @@ +# ============================================================================= # Revert and Re-migrate Workflow +# ============================================================================= +# Runs: revert (clean up previous migration) → migrate (run migration again). +# Use when you need to undo a migration and then re-run with different settings, +# or to clean up a failed/partial migration before trying again. # -# Use this pattern when you need to: -# 1. Clean up a failed or partial migration -# 2. Re-run the migration with corrected settings +# Run: deno task workflow -f examples/workflow-revert-and-migrate.yaml # -# Usage: -# deno task workflow -f workflow-revert-and-migrate.yaml +# When to modify: +# - Before first run: set source/destination to match your migration. +# - revert.dryRun / migration.dryRun: set true to preview; set false to execute. +# - migration.*: same options as other workflows (includeFlags, environments, etc.). +# +# Recommended: Run once with dryRun: true for both revert and migration, then +# set both to false and run again to execute. +# ============================================================================= workflow: steps: - - revert # First, clean up the previous migration - - migrate # Then, run the migration again + - revert # Step 1: Remove migrated flags (and optionally views) from destination + - migrate # Step 2: Run migration again (e.g. with corrected settings) +# ----------------------------------------------------------------------------- +# SOURCE & DESTINATION (required – must match the migration you are reverting) +# ----------------------------------------------------------------------------- +# source.projectKey: project that was the source of the migration (used to +# identify which flags to revert via extracted source data). +# destination.projectKey: project where flags were migrated (flags are deleted here). +# source: - projectKey: source-project + projectKey: Aman-Migration-Test domain: app.launchdarkly.com destination: - projectKey: dest-project + projectKey: aman-migration-2 domain: app.launchdarkly.com -# Revert configuration +# ----------------------------------------------------------------------------- +# REVERT (modify to control cleanup behavior) +# ----------------------------------------------------------------------------- +# Revert uses extracted source data to find migrated flags, then archives/deletes +# them in destination and optionally removes view links. +# revert: - # Preview the revert first (recommended!) + # true = preview only (no changes). false = execute revert. Set false after reviewing. dryRun: true - - # Delete the view after unlinking flags + + # Set true to delete the target view(s) after unlinking all flags. Optional. # deleteViews: true - - # Optional: Only process specific views + + # Only revert flags linked to these views. Omit to use migration.targetView + # from the original migration config (or all migrated flags from source data). # viewKeys: # - migration-view-1 -# Migration configuration +# ----------------------------------------------------------------------------- +# MIGRATION (modify to control the re-migration after revert) +# ----------------------------------------------------------------------------- +# Same options as other workflow files. Use to re-migrate with different +# includeFlags, environments, conflictPrefix, etc. +# migration: assignMaintainerIds: true migrateSegments: true - targetView: "migration-2024" - - # Preview the migration (recommended after revert preview!) + + # View to link migrated flags to. Uncomment if you use a target view. + # targetView: "migration-2024" + + # Migrate only these flag keys. Comment out to migrate all extracted flags. + includeFlags: + - config-welcome-message + + # true = preview only. false = execute migration. Set false after reviewing. dryRun: true - - # Optional: Add conflict prefix + + # Only for initial full migration when keys collide. Do NOT use with incremental sync. # conflictPrefix: "migrated-" - - # Optional: Limit to specific environments + # environments: # - production # - staging - - # Optional: Map environment keys + # environmentMapping: # prod: production # stage: staging -# Workflow execution order: -# 1. Revert (with dryRun: true) - Preview cleanup -# 2. Migrate (with dryRun: true) - Preview re-migration -# -# Then change both dryRun values to false and run again to execute - +# ----------------------------------------------------------------------------- +# EXECUTION ORDER +# ----------------------------------------------------------------------------- +# 1. Revert runs (dryRun: true → preview; dryRun: false → delete flags/cleanup). +# 2. Migrate runs (dryRun: true → preview; dryRun: false → create flags). +# After confirming preview output, set revert.dryRun and migration.dryRun to +# false and run the workflow again to apply changes. diff --git a/src/scripts/launchdarkly-migrations/migrate_between_ld_instances.ts b/src/scripts/launchdarkly-migrations/migrate_between_ld_instances.ts index 7d18745..134425e 100644 --- a/src/scripts/launchdarkly-migrations/migrate_between_ld_instances.ts +++ b/src/scripts/launchdarkly-migrations/migrate_between_ld_instances.ts @@ -36,6 +36,7 @@ interface Arguments { dryRun?: boolean; incremental?: boolean; since?: string; + includeFlags?: string; } interface SyncManifestEnv { @@ -189,6 +190,7 @@ const cliArgs: Arguments = (yargs(Deno.args) .alias("dry-run", "dryRun") .alias("i", "incremental") .alias("since", "since") + .alias("include-flags", "includeFlags") .boolean("m") .boolean("s") .boolean("dry-run") @@ -204,6 +206,7 @@ const cliArgs: Arguments = (yargs(Deno.args) .describe("dry-run", "Preview migration without making any changes") .describe("incremental", "Skip flags unchanged since last sync (version-based)") .describe("since", "Only sync flags modified after this date (ISO 8601, e.g. 2026-01-15)") + .describe("include-flags", "Comma-separated list of flag keys to migrate (default: all extracted flags)") .parse() as unknown) as Arguments; // Load and merge config file if provided @@ -268,6 +271,7 @@ console.log(Colors.gray(` Domain: ${inputArgs.domain || 'app.launchdarkly.com'} console.log(Colors.gray(` Dry Run: ${inputArgs.dryRun || false}`)); console.log(Colors.gray(` Incremental: ${inputArgs.incremental || false}`)); console.log(Colors.gray(` Since: ${inputArgs.since || 'none'}`)); +console.log(Colors.gray(` Include flags: ${inputArgs.includeFlags || 'all'}`)); // Validate --incremental and --since are mutually exclusive if (inputArgs.incremental && inputArgs.since) { @@ -509,10 +513,22 @@ if (!flagList) { console.log(Colors.red(`❌ Error: Could not load flag list from ./data/launchdarkly-migrations/source/project/${inputArgs.projKeySource}/flags.json`)); Deno.exit(1); } -console.log(Colors.green(`✓ Loaded ${flagList.length} flags`)); +let flagsToMigrate: Array = flagList; +if (inputArgs.includeFlags) { + const requested = inputArgs.includeFlags.split(',').map((k) => k.trim()).filter(Boolean); + const availableSet = new Set(flagList); + const notFound = requested.filter((k) => !availableSet.has(k)); + if (notFound.length > 0) { + console.log(Colors.yellow(`⚠ Requested flag(s) not in extracted source (skipped): ${notFound.join(', ')}`)); + } + flagsToMigrate = requested.filter((k) => availableSet.has(k)); + console.log(Colors.green(`✓ Include flags filter: ${flagsToMigrate.length} of ${flagList.length} flags will be migrated`)); +} else { + console.log(Colors.green(`✓ Loaded ${flagList.length} flags`)); +} console.log("Extracting view associations from source flags..."); -for (const flagkey of flagList) { +for (const flagkey of flagsToMigrate) { const flag = await getJson( `./data/launchdarkly-migrations/source/project/${inputArgs.projKeySource}/flags/${flagkey}.json`, ); @@ -528,6 +544,9 @@ if (inputArgs.targetView) { console.log(Colors.cyan(`Target view specified: "${inputArgs.targetView}"`)); } +// View keys that exist in destination (only link flags to these) +const destinationViewKeys = new Set(); + if (allViewKeys.size > 0) { console.log(`Found ${allViewKeys.size} unique view(s) to create/verify: ${Array.from(allViewKeys).join(', ')}`); @@ -539,6 +558,7 @@ if (allViewKeys.size > 0) { if (viewExists) { console.log(Colors.green(` ✓ View "${viewKey}" already exists`)); + destinationViewKeys.add(viewKey); } else { console.log(` Creating view "${viewKey}"...`); const viewData: View = { @@ -551,6 +571,7 @@ if (allViewKeys.size > 0) { if (result.success) { console.log(Colors.green(` ✓ View "${viewKey}" created successfully`)); + destinationViewKeys.add(viewKey); } else { console.log(Colors.yellow(` ⚠ Failed to create view "${viewKey}": ${result.error}`)); } @@ -1301,10 +1322,14 @@ interface Variation { } // Creating Global Flags // -console.log(Colors.blue(`\n🏁 Creating ${flagList.length} flags in destination project...\n`)); -for (const [index, flagkey] of flagList.entries()) { +let flagsCreatedCount = 0; +let flagsUpdatedCount = 0; +const flagsUpdatedKeys: string[] = []; +const migrationStartTime = Date.now(); +console.log(Colors.blue(`\n🏁 Creating ${flagsToMigrate.length} flags in destination project...\n`)); +for (const [index, flagkey] of flagsToMigrate.entries()) { // Read flag - console.log(Colors.cyan(`\n[${index + 1}/${flagList.length}] Processing flag: ${flagkey}`)); + console.log(Colors.cyan(`\n[${index + 1}/${flagsToMigrate.length}] Processing flag: ${flagkey}`)); const flag = await getJson( `./data/launchdarkly-migrations/source/project/${inputArgs.projKeySource}/flags/${flagkey}.json`, @@ -1438,6 +1463,8 @@ for (const [index, flagkey] of flagList.entries()) { flagCreated = true; flagAlreadyExisted = true; createdFlagKey = flagKey; + flagsUpdatedCount++; + flagsUpdatedKeys.push(createdFlagKey); console.log(Colors.gray(`\t⚠ Flag already exists, will update environments...`)); } } else if (checkFlagResp.status === 404) { @@ -1494,18 +1521,16 @@ for (const [index, flagkey] of flagList.entries()) { // Skip the creation POST if flag already exists if (!flagAlreadyExisted) { // Collect view associations for flag creation - // API supports "viewKeys" (plural, array) field during creation (NO beta header needed) + // Only include view keys that exist in destination to avoid "View key not found" 400 const viewKeys: string[] = []; - // Add target view if specified (highest priority) - if (inputArgs.targetView) { + if (inputArgs.targetView && destinationViewKeys.has(inputArgs.targetView)) { viewKeys.push(inputArgs.targetView); } - // Add source flag's view associations (if not already added) if (flag.viewKeys && Array.isArray(flag.viewKeys)) { flag.viewKeys.forEach((viewKey: string) => { - if (!viewKeys.includes(viewKey)) { + if (destinationViewKeys.has(viewKey) && !viewKeys.includes(viewKey)) { viewKeys.push(viewKey); } }); @@ -1529,6 +1554,7 @@ for (const [index, flagkey] of flagList.entries()) { if (flagResp.status == 200 || flagResp.status == 201) { flagCreated = true; createdFlagKey = flagKey; + flagsCreatedCount++; if (viewKeys.length > 0) { console.log(Colors.green(`\t✓ Created and linked to view(s): ${viewKeys.join(', ')}`)); } else { @@ -1553,6 +1579,8 @@ for (const [index, flagkey] of flagList.entries()) { // No prefix or second attempt - flag exists, proceed to update it flagCreated = true; createdFlagKey = flagKey; + flagsUpdatedCount++; + flagsUpdatedKeys.push(createdFlagKey); console.log(Colors.gray(`\t⚠ Flag exists (409), will update environments...`)); break; // Exit retry loop and proceed to patching } @@ -1705,6 +1733,16 @@ for (const [index, flagkey] of flagList.entries()) { }); await makePatchCall(createdFlagKey, patchReq, destEnvKey, flagMaintainerId, currentMemberId, destinationVariations, destinationFlag); + // Log UPDATES for this environment only during incremental sync (not during initial export) + if (inputArgs.incremental) { + const updatedKeys = Object.keys(parsedData).join(', '); + console.log(Colors.gray(` ─────────────────────────────────────`)); + console.log(Colors.cyan(` UPDATES`)); + console.log(Colors.gray(` Flag Key: ${createdFlagKey}`)); + console.log(Colors.gray(` Environment: ${destEnvKey}`)); + console.log(Colors.gray(` Keys: ${updatedKeys}`)); + } + // Track env version in manifest if (envVersion !== undefined) { flagManifestEnvs[env] = { version: envVersion, lastModified: envLastModified }; @@ -1900,3 +1938,35 @@ printMigrationSummary( flagsWithErrors, conflictTracker.getReport() ); + +// Final migration summary banner and result box +const totalTimeSec = ((Date.now() - migrationStartTime) / 1000).toFixed(1); +const conflictReportText = conflictTracker.getReport(); +const noConflicts = !conflictReportText || conflictReportText.trim() === ''; + +console.log(Colors.blue(`\n======================================================================`)); +console.log(Colors.blue(`📊 MIGRATION SUMMARY`)); +console.log(Colors.blue(`======================================================================`)); +if (noConflicts) { + console.log(Colors.gray(`No conflicts encountered during migration.`)); +} else { + console.log(Colors.yellow(`Conflicts were resolved (see conflict report above).`)); +} +console.log(Colors.blue(`\n======================================================================`)); +console.log(Colors.green(`✓ Migration complete successfully`)); +console.log(Colors.blue(`======================================================================\n`)); + +console.log(Colors.cyan(` ┌─────────────────────────────────────────`)); +console.log(Colors.cyan(` │ MIGRATION RESULT`)); +console.log(Colors.cyan(` ├─────────────────────────────────────────`)); +console.log(Colors.cyan(` │ flags created ${String(flagsCreatedCount).padStart(6)}`)); +console.log(Colors.cyan(` │ flags updated ${String(flagsUpdatedCount).padStart(6)}`)); +console.log(Colors.cyan(` │ flags total ${String(flagsCreatedCount + flagsUpdatedCount).padStart(6)}`)); +console.log(Colors.cyan(` │ total time ${totalTimeSec.padStart(5)}s`)); +console.log(Colors.cyan(` └─────────────────────────────────────────\n`)); +if (flagsUpdatedKeys.length > 0) { + console.log(Colors.cyan(` Flags updated: ${flagsUpdatedKeys.join(", ")}`)); + console.log(Colors.gray(` See above for details per flag.`)); +} else { + console.log(Colors.gray(` More detailed changes per flag are logged above.`)); +} diff --git a/src/scripts/launchdarkly-migrations/revert_migration.ts b/src/scripts/launchdarkly-migrations/revert_migration.ts index 718a584..b91018a 100644 --- a/src/scripts/launchdarkly-migrations/revert_migration.ts +++ b/src/scripts/launchdarkly-migrations/revert_migration.ts @@ -197,7 +197,7 @@ const ldAPIDeleteRequest = (apiKey: string, domain: string, path: string) => { }; /** - * Creates a PATCH request for LaunchDarkly API + * Creates a PATCH request for LaunchDarkly API (JSON Patch format, not semantic patch) */ const ldAPIPatchRequest = ( apiKey: string, @@ -207,7 +207,7 @@ const ldAPIPatchRequest = ( useBetaVersion = false ) => { const headers: Record = { - "Content-Type": "application/json; domain-model=launchdarkly.semanticpatch", + "Content-Type": "application/json", "Authorization": apiKey, "User-Agent": "launchdarkly-migration-revert-script", }; @@ -373,6 +373,14 @@ const discoverMigrationFlags = async ( sourceFlags ); + const foundKeys = new Set(destinationFlags.map(f => f.key)); + const notFoundInDest = sourceFlags.filter(k => !foundKeys.has(k)); + if (notFoundInDest.length > 0) { + console.log(Colors.yellow( + ` ${notFoundInDest.length} flag(s) from source were not found in destination (skipped): ${notFoundInDest.join(", ")}` + )); + } + console.log(Colors.gray(` Found ${destinationFlags.length} matching flag(s) in destination`)); // First try: Filter by migration marker in description @@ -735,17 +743,23 @@ const archiveFlag = async ( projectKey: string, flagKey: string, dryRun: boolean -): Promise => { +): Promise<{ success: boolean; errorMessage?: string }> => { if (dryRun) { console.log(Colors.gray(` [DRY RUN] Would archive flag ${flagKey}`)); - return true; + return { success: true }; } const patch = [{ op: "replace", path: "/archived", value: true }]; const req = ldAPIPatchRequest(apiKey, domain, `flags/${projectKey}/${flagKey}`, patch); const response = await rateLimitRequest(req, 'flags'); - return response.status >= 200 && response.status < 300; + if (response.status >= 200 && response.status < 300) { + return { success: true }; + } + + const body = await response.text(); + const errorMessage = `HTTP ${response.status}${body ? `: ${body}` : ""}`; + return { success: false, errorMessage }; }; /** @@ -757,15 +771,21 @@ const deleteFlag = async ( projectKey: string, flagKey: string, dryRun: boolean -): Promise => { +): Promise<{ success: boolean; errorMessage?: string }> => { if (dryRun) { console.log(Colors.gray(` [DRY RUN] Would delete flag ${flagKey}`)); - return true; + return { success: true }; } const req = ldAPIDeleteRequest(apiKey, domain, `flags/${projectKey}/${flagKey}`); const response = await rateLimitRequest(req, 'flags'); - return response.status === 204 || response.status === 200; + if (response.status === 204 || response.status === 200) { + return { success: true }; + } + + const body = await response.text(); + const errorMessage = `HTTP ${response.status}${body ? `: ${body}` : ""}`; + return { success: false, errorMessage }; }; // ==================== Revert Orchestration ==================== @@ -808,7 +828,7 @@ const revertFlag = async ( } // 2. Archive flag - const archived = await archiveFlag( + const archiveResult = await archiveFlag( apiKey, domain, config.projectKey, @@ -816,15 +836,17 @@ const revertFlag = async ( config.dryRun || false ); - if (archived) { + if (archiveResult.success) { stats.flagsArchived++; console.log(Colors.green(` ✓ Archived flag`)); } else { - console.log(Colors.yellow(` ⚠ Could not archive flag`)); + const msg = `Flag ${flag.key}: could not archive${archiveResult.errorMessage ? ` - ${archiveResult.errorMessage}` : ""}`; + stats.errors.push(msg); + console.log(Colors.red(` ✗ ${msg}`)); } - // 3. Delete flag - const deleted = await deleteFlag( + // 3. Delete flag (only if archive succeeded, so we don't try to delete twice on failure) + const deleteResult = await deleteFlag( apiKey, domain, config.projectKey, @@ -832,11 +854,13 @@ const revertFlag = async ( config.dryRun || false ); - if (deleted) { + if (deleteResult.success) { stats.flagsDeleted++; console.log(Colors.green(` ✓ Deleted flag`)); } else { - console.log(Colors.yellow(` ⚠ Could not delete flag`)); + const msg = `Flag ${flag.key}: could not delete${deleteResult.errorMessage ? ` - ${deleteResult.errorMessage}` : ""}`; + stats.errors.push(msg); + console.log(Colors.red(` ✗ ${msg}`)); } } catch (error) { @@ -937,16 +961,19 @@ const printRevertSummary = (config: RevertConfig, stats: RevertStats): void => { } console.log(Colors.cyan("\nStatistics:")); - console.log(Colors.gray(` Flags checked: ${stats.flagsChecked}`)); + console.log(Colors.gray(` Flags checked (source/destination): ${stats.flagsChecked}`)); console.log(Colors.cyan(` Migration flags identified: ${stats.flagsIdentified}`)); - console.log(Colors.green(` Approval requests deleted: ${stats.approvalsDeleted}`)); - console.log(Colors.green(` View links removed: ${stats.viewLinksRemoved}`)); - console.log(Colors.green(` Flags archived: ${stats.flagsArchived}`)); - console.log(Colors.green(` Flags deleted: ${stats.flagsDeleted}`)); - console.log(Colors.green(` Views deleted: ${stats.viewsDeleted}`)); + console.log(Colors.green(` Flags reverted (archived + deleted): ${stats.flagsDeleted} / ${stats.flagsIdentified}`)); + if (stats.flagsIdentified > 0 && stats.flagsDeleted < stats.flagsIdentified) { + const notReverted = stats.flagsIdentified - stats.flagsDeleted; + console.log(Colors.yellow(` ${notReverted} flag(s) could not be reverted (see errors below)`)); + } + console.log(Colors.gray(` Approval requests deleted: ${stats.approvalsDeleted}`)); + console.log(Colors.gray(` View links removed: ${stats.viewLinksRemoved}`)); + console.log(Colors.gray(` Views deleted: ${stats.viewsDeleted}`)); if (stats.errors.length > 0) { - console.log(Colors.red(`\n❌ Errors encountered: ${stats.errors.length}`)); + console.log(Colors.red(`\n❌ Errors / failures (${stats.errors.length}):`)); stats.errors.forEach(err => { console.log(Colors.red(` • ${err}`)); }); @@ -959,7 +986,7 @@ const printRevertSummary = (config: RevertConfig, stats: RevertStats): void => { } else if (stats.errors.length === 0) { console.log(Colors.green("✓ Revert completed successfully")); } else { - console.log(Colors.yellow("⚠ Revert completed with errors")); + console.log(Colors.yellow("⚠ Revert completed with errors (see above for details)")); } console.log(Colors.blue(`${divider}\n`)); diff --git a/src/scripts/launchdarkly-migrations/workflow.ts b/src/scripts/launchdarkly-migrations/workflow.ts index 0f86b0e..d78cada 100644 --- a/src/scripts/launchdarkly-migrations/workflow.ts +++ b/src/scripts/launchdarkly-migrations/workflow.ts @@ -40,6 +40,7 @@ interface WorkflowConfig { dryRun?: boolean; incremental?: boolean; since?: string; + includeFlags?: string[]; }; thirdPartyImport?: { inputFile: string; @@ -252,6 +253,7 @@ const buildMigrationArgs = (config: WorkflowConfig): string[] => { args = addOptionalArg(args, "-v", migration.targetView); args = addOptionalArg(args, "-e", migration.environments?.join(",")); args = addOptionalArg(args, "--since", migration.since); + args = addOptionalArg(args, "--include-flags", migration.includeFlags?.join(",")); if (migration.environmentMapping) { args = addOptionalArg(args, "--env-map", formatEnvMapping(migration.environmentMapping)); From 4b8d15fff7948eba34b476dad63b668fbc6cdfd4 Mon Sep 17 00:00:00 2001 From: akaur-ld Date: Thu, 5 Mar 2026 15:50:37 -0500 Subject: [PATCH 2/2] Add delta logging (jsondiffpatch), same-env migration in yaml --- ld-migration-scripts | 1 + 1 file changed, 1 insertion(+) create mode 160000 ld-migration-scripts diff --git a/ld-migration-scripts b/ld-migration-scripts new file mode 160000 index 0000000..4b81f6b --- /dev/null +++ b/ld-migration-scripts @@ -0,0 +1 @@ +Subproject commit 4b81f6b9be7eb160c245198ab03e83d82ec170af