[AJDA-3117] Add manage:migrate-orchestrations-to-flow batch driver - #113
Conversation
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.
|
There was a problem hiding this comment.
Pull request overview
Adds a new Symfony Console command (manage:migrate-orchestrations-to-flow) to drive the keboola.orchestrator → keboola.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
MigrateOrchestrationsToFlowcommand with project list resolution, CSV reporting, and summary/exit-code semantics. - Implements
FlowMigration\BatchRunner+ DTOs/factory to submit and supervisekeboola.flow-migration-tooljobs 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.
odinuv
left a comment
There was a problem hiding this comment.
tak jako mergnutelný to je, ale radši bych to viděl seškrtaný na čtvrtinu
| * | ||
| * @return array<int, string>|null null when any remaining line is not a plain non-negative integer | ||
| */ | ||
| private function parseProjectIdsFile(string $contents): ?array |
There was a problem hiding this comment.
jestli by něco stálo za to tak tohle vytáhnout z těch commandů na jedno místo, opakuje se to tu
There was a problem hiding this comment.
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í -> chybaRozchá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 |
There was a problem hiding this comment.
Nepřináší, smazáno.
| $this->assertCount(1, $this->results); | ||
| } | ||
|
|
||
| public function testNonPositiveConcurrencyStillDrainsTheQueueInsteadOfHanging(): void |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
tohle bych seškrtal tak napolovinu - ja nevím no třeba testWarningJobCountsAsMigratedWithWarning - proč?
There was a problem hiding this comment.
Seškrtáno ze 17 na 11 metod (celá suite 62 -> 52 testů).
testWarningJobCountsAsMigratedWithWarning je pryč — měl jsi pravdu, že nemá smysl: ProjectResultTest už warning 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 jakotestManageErrorOtherThan404testPollFailureCounterResetsAfterASuccessfulPoll— tři testy na jednu toleranci pollu byly dva navíctestNonPositiveConcurrency...atestSkipsProjectWithLiveMigrationJob— 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()(flagisDisabledi 404) zůstávají pokryté
Co jsem nechal a proč, kdyby to bylo pořád moc:
testTransportFailureOnProjectLookupDoesNotAbortTheBatch— regrese na reálný bug:ConnectExceptionneníManageClientException, takže propadal zrun()a shodil celý batchtestWithoutForceJobRunsWithDryRunTrue—dryRunje tady ta bezpečnostně kritická vlajkatestConcurrencyWindowCapsInFlightJobsAndRefills— 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 BatchRunner už listJobs nevolá a override z fake klienta zmizel i se svým parametrem konstruktoru. Problém, na který ukazuješ, tím přestal existovat.
|
co se týče tohle:
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.
Adds
manage:migrate-orchestrations-to-flow, a batch driver for the automatedkeboola.orchestrator->keboola.flowmigration, 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-toolcomponent (
keboola/flow-migration-tool); nothing from it is reimplemented here.Usage
<projects>is a comma-separated list of project IDs or@path/to/filewith one ID per line(blank lines and
#comments allowed, duplicates removed).<token> <url>argument order keepsthe command compatible with
manage:call-on-stacks.Per-project flow
getProject()— disabled and deleted (404) projects are skipped before any token is created.canManageBuckets,canReadAllFileUploads,componentAccess for orchestrator/flow/scheduler/flow-migration-tool). It expires on its own, so
there is no cleanup step.
keboola.orchestratorconfigurations — zero configurations skips the project withoutcreating a job, so empty projects never show up in customers' job history.
keboola.flow-migration-tooljob is alreadycreated/waiting/processing/terminatingthere, so a re-run cannot overlap a live batch.configData(mode: project,skipBroken: true), leaving no storedconfiguration behind in the project.
--concurrencyjobs in flight and poll each every--poll-intervalseconds,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:
Note on dry-run
Dry-run is the default and
-fis required for the real migration, but unlike the usual cli-utilsdry-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 theCommand/FlowMigration/subnamespace:FlowMigration\BatchRunner— all batch logic (skip rules, concurrency window, polling), unittested against fakes
FlowMigration\ProjectClientsFactory— the only network seam; creates the ephemeral token andthe per-project clients bound to it
FlowMigration\ProjectClients,FlowMigration\ProjectResult— DTOsTests mirror that layout in
tests/FlowMigration/, faking the SDK clients by subclassing themwithout calling
parent::__construct()— the same pattern as the existingFakeComponents.Verification
composer tests— 62 tests, 161 assertionscomposer phpstan— level 9, no errorsphpcs --standard=psr2— cleanconnection.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:
keboola.flow-migration-toolis excluded from billing in the Developer Portal (applies todry-run too, since it also creates a real job).
SourceNotificationLoadercan listproject-subscriptionswith an ephemeral token — if thelisting is scoped to the token's user, notification migration silently does nothing.
TriggerWritermay create triggers with arunWithTokenIdother than the caller's.expiresInoutlasts time spent queued, not just the run itself.keboola.flowneeds no per-project feature on the target stacks; if it does, a pre-check has toskip such projects.
Closes AJDA-3117.