Skip to content

[AJDA-3117] Add manage:migrate-orchestrations-to-flow batch driver - #113

Merged
ondrajodas merged 5 commits into
mainfrom
ondra/AJDA-3117
Aug 12, 2026
Merged

[AJDA-3117] Add manage:migrate-orchestrations-to-flow batch driver#113
ondrajodas merged 5 commits into
mainfrom
ondra/AJDA-3117

Conversation

@ondrajodas

Copy link
Copy Markdown
Contributor

Adds manage:migrate-orchestrations-to-flow, a batch driver for the automated
keboola.orchestrator -> keboola.flow migration, so per-stack migrations (AJDA-3119, AJDA-3120)
become a single supervised run instead of manual per-project work.

The command is only a driver. All migration logic lives in the keboola.flow-migration-tool
component (keboola/flow-migration-tool); nothing from it is reimplemented here.

Usage

php cli.php manage:migrate-orchestrations-to-flow [-f|--force] <token> <url> [<projects>] \
    [--projects-file=PATH] [--concurrency=10] [--poll-interval=5] [--report=PATH]

<projects> is a comma-separated list of project IDs or @path/to/file with one ID per line
(blank lines and # comments allowed, duplicates removed). <token> <url> argument order keeps
the command compatible with manage:call-on-stacks.

Per-project flow

  1. getProject() — disabled and deleted (404) projects are skipped before any token is created.
  2. Create an ephemeral project storage token (12 h, canManageBuckets, canReadAllFileUploads,
    componentAccess for orchestrator/flow/scheduler/flow-migration-tool). It expires on its own, so
    there is no cleanup step.
  3. List keboola.orchestrator configurations — zero configurations skips the project without
    creating a job, so empty projects never show up in customers' job history.
  4. Skip the project when a keboola.flow-migration-tool job is already
    created/waiting/processing/terminating there, so a re-run cannot overlap a live batch.
  5. Create the job through configData (mode: project, skipBroken: true), leaving no stored
    configuration behind in the project.
  6. Keep at most --concurrency jobs in flight and poll each every --poll-interval seconds,
    refilling the window as jobs finish.

A failing project never aborts the batch; the exit code is 1 when at least one project failed.
Re-running the same list is the intended recovery path — the component reports already-migrated
orchestrations as skipped and step 4 skips projects with a live job, so no resume state is kept.

Report

A CSV is appended as each project resolves (so an interrupted run stays auditable), with one row
per input project including skips:

projectId;jobId;status;durationSeconds;error

Note on dry-run

Dry-run is the default and -f is required for the real migration, but unlike the usual cli-utils
dry-run this one is not side-effect free: the component only runs as a job, so even in dry-run
mode a real job and a real ephemeral token are created in every eligible project. On PAYGO stacks
that consumes customer credits. The command prints this notice at startup and the README documents
it.

Architecture

Only the Symfony command sits in src/Keboola/Console/Command/; its helpers live in the
Command/FlowMigration/ subnamespace:

  • FlowMigration\BatchRunner — all batch logic (skip rules, concurrency window, polling), unit
    tested against fakes
  • FlowMigration\ProjectClientsFactory — the only network seam; creates the ephemeral token and
    the per-project clients bound to it
  • FlowMigration\ProjectClients, FlowMigration\ProjectResult — DTOs

Tests mirror that layout in tests/FlowMigration/, faking the SDK clients by subclassing them
without calling parent::__construct() — the same pattern as the existing FakeComponents.

Verification

  • composer tests — 62 tests, 161 assertions
  • composer phpstan — level 9, no errors
  • phpcs --standard=psr2 — clean
  • Both modes were run end-to-end against connection.canary-orion.keboola.dev, project 302:
    dry-run (job 614504, success) and force (job 614517, success), each writing the expected CSV row
    and exiting 0. An invalid token was also verified to produce a per-project error row without
    aborting the batch.

Still to confirm before the first live batch

These are properties of the component and the platform rather than of this command, and need a live
project to confirm:

  1. keboola.flow-migration-tool is excluded from billing in the Developer Portal (applies to
    dry-run too, since it also creates a real job).
  2. SourceNotificationLoader can list project-subscriptions with an ephemeral token — if the
    listing is scoped to the token's user, notification migration silently does nothing.
  3. TriggerWriter may create triggers with a runWithTokenId other than the caller's.
  4. The chosen expiresIn outlasts time spent queued, not just the run itself.
  5. keboola.flow needs no per-project feature on the target stacks; if it does, a pre-check has to
    skip such projects.

Closes AJDA-3117.

Drives keboola.flow-migration-tool jobs across a list of projects on one
stack; all migration logic stays in the component. Per project it creates an
ephemeral storage token, skips disabled/deleted projects, projects without
keboola.orchestrator configurations and projects with a live migration job,
then supervises the job in a bounded concurrency window.

Every input project gets a CSV row appended the moment it resolves, so an
interrupted run stays auditable. Dry-run by default behind -f/--force (the
job itself runs with dryRun: true), a failing project never aborts the batch
and the exit code is 1 when any project failed.
@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown

AJDA-3117

@ondrajodas

Copy link
Copy Markdown
Contributor Author
# Release Notes
New internal CLI command `manage:migrate-orchestrations-to-flow` in keboola/cli-utils. It runs the
automated keboola.orchestrator -> keboola.flow migration across a batch of projects on one stack:
for every project it creates an ephemeral storage token, checks whether the project has anything to
migrate, creates a keboola.flow-migration-tool job through configData, and supervises the jobs with
a bounded concurrency window. Results land in an incrementally appended CSV report
(projectId;jobId;status;durationSeconds;error) plus a stdout summary. Dry-run is the default; -f
performs the real migration. No customer-facing product change.

# Plans for Customer Communication
No customer communication for this PR itself — it only adds an internal operator tool and changes
nothing in the product. Customer communication belongs to the per-stack migration tickets
(AJDA-3119 Azure NE, AJDA-3120 GCP US), which is where orchestrations actually get migrated.
Two things to settle there before the first live batch:
- The migration job runs inside the customer's project, so it appears in their job history (in
  dry-run too). On PAYGO stacks it may consume their credits unless the component is excluded from
  billing.
- Migrated orchestrations show up as Conditional Flows in the customer's project.

# Impact Analysis
Affected: keboola/cli-utils only. No shared library, service, or deployed component is modified.

The command touches customer projects at runtime through public APIs:
- Manage API — getProject(), createProjectStorageToken()
- Storage API — listing keboola.orchestrator configurations
- Job Queue API — listing live jobs, creating and polling the migration job

Risks:
- Blast radius is always an explicit project list; there is no "all projects" mode.
- Even without -f a real job and a real ephemeral storage token are created in every eligible
  project. Not free of side effects, unlike the usual cli-utils dry-run.
- The ephemeral token is deliberately broad (canManageBuckets, canReadAllFileUploads,
  componentAccess for orchestrator/flow/scheduler/flow-migration-tool) and lives 12 h. It expires
  on its own; there is no revocation step.
- Actual migration correctness is the component's responsibility (keboola/flow-migration-tool), not
  this driver's.
- Concurrency defaults to 10 jobs in flight, which puts load on the target stack's job queue.

# Change Type
Feature — new internal CLI command, plus its unit tests and README documentation. No breaking
changes; no existing command, class, or dependency was modified (composer.json untouched, all
required SDKs were already installed).

# Justification
Per-stack orchestrator -> flow migrations (AJDA-3119, AJDA-3120) would otherwise mean manual,
unrepeatable per-project work across hundreds of projects, with no audit trail. This command turns
each stack into a single supervised run with a CSV report, skip rules that keep empty projects out
of customers' job history, and a re-run path that is safe by construction.

Linear: https://linear.app/keboola/issue/AJDA-3117

# Deployment Plan
No deployment in the usual sense — cli-utils is run from the Docker image, not deployed as a
service. Merging to main is enough; the image is tagged and pushed by CI on tag builds.

No config change, no migration, no infrastructure change. No composer.json change.

Before the first live batch, confirm on a single project (a one-project batch is the intended way
to do this):
1. keboola.flow-migration-tool is excluded from billing in the Developer Portal — this applies to
   dry-run too, since it also creates a real job.
2. SourceNotificationLoader can list project-subscriptions with an ephemeral token. If that listing
   is scoped to the token's user, notification migration silently does nothing and the tool reports
   "nothing to migrate" rather than an error.
3. TriggerWriter is allowed to create triggers with a runWithTokenId other than the caller's.
4. The 12 h expiresIn outlasts time spent queued, not just the job runtime.
5. keboola.flow needs no per-project feature on the target stacks; if it does, a pre-check skipping
   such projects has to be added.

Both modes have already been verified end-to-end on connection.canary-orion.keboola.dev, project
302: dry-run (job 614504, success) and force (job 614517, success).

# Rollback Plan
For the PR: revert the merge commit. Nothing else depends on the new code — the command is
registered in cli.php and referenced by nothing else, and no shared code path was changed.

For a batch already in flight: interrupt the process. Jobs already created keep running server-side,
but no new ones are submitted, and the CSV written so far stays valid and auditable.

For a migration already performed: this driver cannot undo it — reverting migrated flows is the
component's domain (keboola.flow configurations would have to be removed in the affected projects,
using the CSV report to identify them). Ephemeral tokens need no cleanup; they expire on their own
within 12 h.

# Post-Release Support Plan
Each run is supervised by the operator in the foreground, so monitoring is the run's own output:
- The CSV report is the audit trail — one row per input project, appended as each project resolves,
  so it stays usable even if the run is interrupted. Keep it per stack.
- Exit code 1 means at least one project failed; the failing rows carry the job ID, so every failure
  is traceable to a job in the customer's project.
- Re-running the same project list is the supported recovery path: already-migrated orchestrations
  are reported as skipped by the component, and projects with a live migration job are skipped by
  the driver.

Known edge cases to watch:
- Job-level status only. The component does not expose per-orchestration counts in the job result,
  so migrated/skipped/failed orchestration counts are visible only in the job log. Accepted for now;
  a follow-up in keboola/flow-migration-tool could expose them.
- A job status of "warning" is counted as migrated and does not fail the run — worth reading those
  job logs.
- Polling tolerates 3 consecutive failures per job, then marks the project failed while the job
  keeps running server-side. Such a row means "unknown outcome", not "did not run" — check the job.
- There is no per-job timeout; a stuck job holds its concurrency slot until the operator intervenes.

@ondrajodas

Copy link
Copy Markdown
Contributor Author

@ondrajodas

Copy link
Copy Markdown
Contributor Author

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a new Symfony Console command (manage:migrate-orchestrations-to-flow) to drive the keboola.orchestratorkeboola.flow migration as a supervised batch run per stack. The implementation follows the repo’s pattern of keeping CLI wiring in the Command and extracting testable orchestration logic into a dedicated helper class.

Changes:

  • Introduces MigrateOrchestrationsToFlow command with project list resolution, CSV reporting, and summary/exit-code semantics.
  • Implements FlowMigration\BatchRunner + DTOs/factory to submit and supervise keboola.flow-migration-tool jobs with bounded concurrency and polling.
  • Adds unit tests and fakes covering the batch runner behavior and key command helper parsing; documents the command in README and adds internal design/plan docs.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/MigrateOrchestrationsToFlowTest.php Tests command-private helpers (URL parsing, project list parsing) and CSV row escaping behavior.
tests/FlowMigration/ProjectResultTest.php Verifies ProjectResult skip/fail classification rules.
tests/FlowMigration/FakeProjectClientsFactory.php In-memory fake for per-project client creation and project lookup.
tests/FlowMigration/FakeJobQueueClientTest.php Tests scripted fake Queue client behavior.
tests/FlowMigration/FakeJobQueueClient.php Scriptable in-memory JobQueueClient fake and Job DTO builder.
tests/FlowMigration/BatchRunnerTest.php Exercises batch submission, skip rules, concurrency window, and polling/failure tolerance.
src/Keboola/Console/Command/MigrateOrchestrationsToFlow.php New CLI command: input validation, report file handling, runner wiring, and summary/exit code.
src/Keboola/Console/Command/FlowMigration/ProjectResult.php DTO for per-project outcomes, including skip/failure helpers.
src/Keboola/Console/Command/FlowMigration/ProjectClientsFactory.php Network seam that creates ephemeral project tokens and per-project API clients.
src/Keboola/Console/Command/FlowMigration/ProjectClients.php DTO holding per-project Storage/Queue clients.
src/Keboola/Console/Command/FlowMigration/BatchRunner.php Core batch orchestration: skip checks, job submission, polling, and summary aggregation.
README.md User-facing documentation for running the new batch driver command.
docs/superpowers/specs/2026-08-10-migrate-orchestrations-to-flow-design.md Design/spec document capturing architecture and operational semantics.
docs/superpowers/plans/2026-08-10-migrate-orchestrations-to-flow.md Implementation plan and rationale for the approach and testing strategy.
cli.php Registers the new command in the application entrypoint.

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

Comment thread src/Keboola/Console/Command/MigrateOrchestrationsToFlow.php
@ondrajodas
ondrajodas marked this pull request as ready for review August 11, 2026 09:14
@ondrajodas
ondrajodas requested a review from odinuv August 11, 2026 09:15

@odinuv odinuv left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tak jako mergnutelný to je, ale radši bych to viděl seškrtaný na čtvrtinu

Comment thread docs/superpowers/plans/2026-08-10-migrate-orchestrations-to-flow.md Outdated
Comment thread docs/superpowers/specs/2026-08-10-migrate-orchestrations-to-flow-design.md Outdated
Comment thread src/Keboola/Console/Command/FlowMigration/ProjectClientsFactory.php Outdated
Comment thread src/Keboola/Console/Command/FlowMigration/ProjectClientsFactory.php Outdated
Comment thread tests/MigrateOrchestrationsToFlowTest.php Outdated
*
* @return array<int, string>|null null when any remaining line is not a plain non-negative integer
*/
private function parseProjectIdsFile(string $contents): ?array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

jestli by něco stálo za to tak tohle vytáhnout z těch commandů na jedno místo, opakuje se to tu

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Souhlasím, že se to opakuje, ale v tomhle PR to dělat nebudu — je to refaktor napříč devíti existujícími příkazy a mění chování produkčních nástrojů.

Podíval jsem se na to a problém je, že se to opakuje pokaždé jinak:

DeletedProjectsPurge      array_filter(explode(',', $ids), 'is_numeric')
OrganizationsAddFeature   array_filter(explode(',', $ids), 'is_numeric')
ProjectsRemoveFeature     array_filter(explode(',', $ids), 'is_numeric')
DeleteStorageBackend      array_filter(array_map('trim', explode(',', $ids)), 'is_numeric')
DeleteOrgOrphanedWorksp.  array_map('intval', array_filter(explode(',', $ids), 'is_numeric'))
DeleteProjects            trim + posbírá nevalidní a skončí chybou
MigrateDataApps...        trim + ctype_digit, nevalidní -> chyba

Rozcházejí se ve čtyřech věcech naráz: trimování, validátor (is_numeric vs ctype_digit), co s nevalidní hodnotou (tiše zahodit vs. spadnout) a typ výstupu (string vs. int).

Ta první varianta je přitom latentní bug, ne jen nekonzistence: is_numeric pustí 12e4 (což je 120000) i 1.5, a 123x se tiše zahodí — operátor nedostane signál, že mu ze seznamu vypadlo ID. U DeletedProjectsPurge je to zvlášť nepříjemné.

Sjednocení tedy znamená u části příkazů změnit tiché zahazování na tvrdou chybu, což je změna chování, kterou chci udělat vědomě a otestovat, ne přibalit k nové feature. Zatím jsme se na tom s @ondrajodas shodli, že to řešit nebudeme.

use PHPUnit\Framework\TestCase;
use RuntimeException;

class FakeJobQueueClientTest extends TestCase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

test mocku ? přináší to něco?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nepřináší, smazáno.

Comment thread tests/FlowMigration/BatchRunnerTest.php Outdated
$this->assertCount(1, $this->results);
}

public function testNonPositiveConcurrencyStillDrainsTheQueueInsteadOfHanging(): void

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

bych rovnou smazal

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Smazáno — a s ním i ten max(1, $concurrency) clamp, který testoval. Command --concurrency validuje na >= 1 a BatchRunner nemá jiného volajícího, takže clamp hlídal vstup, který přes CLI nemůže nastat. Na konstruktoru je místo toho docblock, že >= 1 garantuje volající.

use RuntimeException;
use Symfony\Component\Console\Output\BufferedOutput;

class BatchRunnerTest extends TestCase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

tohle bych seškrtal tak napolovinu - ja nevím no třeba testWarningJobCountsAsMigratedWithWarning - proč?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Seškrtáno ze 17 na 11 metod (celá suite 62 -> 52 testů).

testWarningJobCountsAsMigratedWithWarning je pryč — měl jsi pravdu, že nemá smysl: ProjectResultTestwarning pokrývá v truth-table statusů, takže tady jen znovu ověřoval mapování na counter.

Dál jsem odstranil:

  • testDriverSideErrorWhenTokenCreationFails... — stejný catch blok jako testManageErrorOtherThan404
  • testPollFailureCounterResetsAfterASuccessfulPoll — tři testy na jednu toleranci pollu byly dva navíc
  • testNonPositiveConcurrency... a testSkipsProjectWithLiveMigrationJob — s kódem, který testovaly
  • testy pro disabled a deleted projekt jsem sloučil do jednoho, běží jako jeden batch se dvěma projekty, takže obě větve checkProjectIsActive() (flag isDisabled i 404) zůstávají pokryté

Co jsem nechal a proč, kdyby to bylo pořád moc:

  • testTransportFailureOnProjectLookupDoesNotAbortTheBatch — regrese na reálný bug: ConnectException není ManageClientException, takže propadal z run() a shodil celý batch
  • testWithoutForceJobRunsWithDryRunTruedryRun je tady ta bezpečnostně kritická vlajka
  • testConcurrencyWindowCapsInFlightJobsAndRefills — okno je jediná netriviální logika, kterou runner má

Jestli chceš jít blíž k polovině, obětoval bych ještě testDeduplicatesInputProjectIds a jeden z dvojice poll-failure testů.

return $outcome;
}

public function listJobs(ListJobsOptions $listOptions): array

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

jako přijde mě to spíš negativní než pozitivní - tenhle mock vždycky vrátí listJobsReturn akorátže nemá žádný expect, takže je to fuk co do něj přijde a test bude vždycky procházet

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Přesná námitka a byla nejvěcnější z celého review — ten fake vracel listJobsReturn bez ohledu na dotaz, takže guard ptající se na špatnou komponentu nebo špatné statusy by testem prošel.

Nejdřív jsem to opravil tak, že fake zaznamenával $listOptions->getQueryParameters() a test asertoval přesný dotaz (ověřeno tím, že po záměně komponenty na keboola.wrong-component test spadl).

Nakonec je to ale vyřešené radikálněji: guard jsme podle tvého komentáře výš zrušili celý, takže BatchRunnerlistJobs nevolá a override z fake klienta zmizel i se svým parametrem konstruktoru. Problém, na který ukazuješ, tím přestal existovat.

@odinuv

odinuv commented Aug 11, 2026

Copy link
Copy Markdown
Member

co se týče tohle:

keboola.flow-migration-tool is excluded from billing in the Developer Portal (applies to
dry-run too, since it also creates a real job).

tak není a neřešil bych to, ta migrace trvá pár minut max, ne?

- ephemeral project token: 12 h -> 1 h, and full project rights
  (canManageBuckets, canManageTokens, canReadAllFileUploads, canPurgeTrash).
  componentAccess is dropped: it is not part of the Manage API project token
  contract, so restricting the token that way had no effect. The job runs for
  minutes and, started from configData, waits only in the shared queue, so an
  hour covers the whole run with a fully privileged token kept short-lived.
- FakeJobQueueClient records the listJobs() query parameters and the live-job
  guard test asserts them; the scripted return is independent of the query, so
  a guard asking for the wrong component or statuses used to pass regardless.
- drop the unreachable max(1, concurrency) clamp - the command already rejects
  a non-positive --concurrency, so the runner has no caller that can hit it.
- trim tests: merge the disabled/deleted skip cases into one batch, drop
  coverage already provided elsewhere, and remove the fake's own test suite.
- stop tracking the internal design and plan docs under docs/superpowers.
The guard queried the Queue API for a live keboola.flow-migration-tool job in
the project and skipped it. It was never mutual exclusion - createJob can still
land right after listJobs returns empty - and the case it did cover (re-running
an interrupted batch while its jobs are still in flight) is covered operationally
by letting those jobs finish first, which the README now says explicitly.

Removes the guard, its ProjectResult status, its summary counter and the
listJobs() override in the fake queue client, which the runner no longer calls.
@ondrajodas
ondrajodas merged commit d40c103 into main Aug 12, 2026
1 check passed
@ondrajodas
ondrajodas deleted the ondra/AJDA-3117 branch August 12, 2026 11:09
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.

3 participants