BitzOrcas.Workflow.Migrator is platform Tooling that migrates workflow definitions only. It reads either the BitzOrcas five-table set or the Saury T_WorkflowDefinition* tables, selected by source, and converts them to the new engine JSON DSL. Outside dry run it calls the real Workflow IRepositoryService for DeployAsync and PublishDeploymentAsync. Saury table knowledge stays in Tooling and must not enter Framework or Platform runtime projects.
Migration stages
How to read the chart: after configuration is approved, resolve the source, then split by table family. BitzOrcas keeps the existing five-table converter unchanged. Saury uses the step-tree converter and also writes a sidecar report. output-dir only controls disk output. DryRun decides whether to deploy. Warnings do not increment the failure count, and a successful conversion is not semantic equivalence.
Prepare
- The source database is a read-only snapshot with a recorded time and schema version.
- The target is an isolated migration database, not a production tenant database.
- Choose
source. When omitted, detection usesFlowDefineforbitzorcasandT_WorkflowDefinitionforsaury. Both tables fall back tobitzorcas. Neither table throws and exits with 99. - Set
flow-namesexplicitly. BitzOrcas filtersFlowDefine.Name; Saury filtersT_WorkflowDefinition.Name. Empty migrates every active definition. - For Saury office-scoped permissions and tenant bindings, prepare an integer
office-id. - Freeze legacy definition edits; the tool does not lock source tables.
- Define instance cutover; this tool does not move
T_Workflowor BitzOrcas in-flight instances.
# Build the tool and Workflow dependencies to expose metadata or adapter drift.dotnet build src/Tooling/BitzOrcas.Workflow.Migrator \ --configuration Release
# Converter and DSL compile tests.dotnet test tests/BitzOrcas.Workflow.Migrator.Tests \ --configuration ReleaseMissing either connection string prints usage and returns 1. There is no dedicated help switch.
Configuration wiring
The program loads appsettings.json, appsettings.local.json, MIGRATOR_ environment variables, and the command line, binds the Migrator section, then applies kebab-case switches through MigratorCli.ApplyOverrides. Command-line switches win over the already bound JSON and environment values.
{ "Migrator": { "SourceConnectionString": "Server=legacy-staging;Database=WorkflowSnapshot;User Id=wf_migrator;Password=Secr3t!P@ss;TrustServerCertificate=True", "TargetConnectionString": "Server=target-isolated;Database=WorkflowMigration;User Id=wf_migrator;Password=Secr3t!P@ss;TrustServerCertificate=True", "TenantId": "PLATFORM", "DryRun": true, "FlowNames": "MatterApproval,SealApply", "Source": "saury", "OfficeId": "1", "OutputDir": "/tmp/wf-migrator-out" }}Save secrets as appsettings.local.json in the tool directory and confirm Git ignores it. Password/Pwd is masked in the summary log, but exception, shell, and driver logs remain sensitive artifacts.
| Switch | Options key | Meaning |
|---|---|---|
source-connection / target-connection | SourceConnectionString / TargetConnectionString | Source and target databases |
source | Source | bitzorcas or saury; empty triggers detection |
office-id | OfficeId | Saury only: filter permissions and T_TenantWorkflowDefinition |
output-dir | OutputDir | Write {key}.v{version}.json and the sidecar report |
flow-names | FlowNames | Comma-separated flow names |
tenant-id | TenantId | Target tenant, default PLATFORM |
dry-run | DryRun | Convert without deploy |
Implement
Run from the tool project directory so local configuration is loaded.
cd src/Tooling/BitzOrcas.Workflow.Migrator
# Existing BitzOrcas invocation: omit source and stay on the five-table path.dotnet run --configuration Release -- \ --dry-run \ --output-dir /tmp/wf-bitzorcas
# Saury product database: explicit source, optional name and office filters.dotnet run --configuration Release -- \ --source saury \ --dry-run \ --office-id 1 \ --flow-names MatterApproval \ --output-dir /tmp/wf-sauryFor both sources, output-dir writes {key}.v{version}.json, {key}.v{version}.migration-report.md, and {key}.migration-report.md. Omit it to keep console-only output.
BitzOrcas five-table path
FlowDefine selects IsDeleted = 0 OR IsDeleted IS NULL. Per definition the tool reads nodes ordered by NodeStep, edges with LaneType = 0, conditions joined through FlowControl, and all approvers. FlowDefinitionConverter node, countersign, rollback, and condition mapping is unchanged by the Saury path.
| Legacy NodeType | JSON DSL type | Notes |
|---|---|---|
Start | startEvent | Entry |
Approver | userTask | Approval task |
Finish | endEvent | Exit |
| other values | userTask | Warning only, then degrade |
Conditions map GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Equal, and NotEqual. Contains returns empty, so the edge has no condition.
Saury definition path
SauryFlowReader reads T_WorkflowDefinition, T_WorkflowDefinitionStep, T_WorkflowDefinitionState, T_WorkflowPermission, T_WorkflowOrganizationUnits, and T_TenantWorkflowDefinition. It does not read T_Workflow instances. Only IsActive = 1 definitions are loaded, ordered by Name then Version.
Version resolution matches DefinitionManager.GetDefinitionState: prefer the DefinitionId bound in T_TenantWorkflowDefinition, otherwise fall back to Version = 1, then order by Version descending. Publish uses the highest Version with a null office when there is no binding; bound offices publish the matching version.
| Saury | New DSL | Rule |
|---|---|---|
Name / Version / display name | key / version / name | key = SanitizeKey(Name), same cleanup as BitzOrcas |
| Top-level step chain | nodes + edges | Prepend start, append end |
WaitFor | userTask | Map permissions by WaitForId; office-id filters OfficeId; missing permission omits participants and warns |
T_WorkflowOrganizationUnits | dataScope | OfficeLevel 1/2/3 maps to Tenant/Office/Department; non-empty org lists map to Custom |
If | exclusiveGateway | Consecutive If steps become one gateway; Condition is translated mechanically |
Action type name Return / Redo | rollback | Return prefers mode: start; Redo uses prev |
Action type name Approve / Confirm / End | Folded into an edge | Side effects stay out of the DSL; StateId goes to the report |
Leading Apply | Folded | Applicant submit is starter logic |
While / ForEach / Schedule / Recur | Not migrated | Warning only |
T_WorkflowDefinitionState | Sidecar state map | Not written into the DSL |
Control primitives are detected by the WorkflowCore.Primitives. prefix, with or without an assembly suffix. Condition translation keeps Data.X / data.X, comparison and logical operators, numbers, quoted strings, and true / false / null. Method calls such as .Contains(value) are not translated: the edge has no condition and the original text goes to warnings plus the sidecar.
Sidecar migration report
Each Saury definition writes one Markdown file for business modules implementing IBusinessIntegrationCallback.OnStatusChangedAsync against BusinessStatusChange.ToFlowState and ToNodePrefix:
- State map: legacy StateId / Name / DisplayName / IsAudit / Category versus new NodePrefix and suggested FlowState.
- Hook list: each business step Name, DisplayName, full StepType, IsAudit, owning WaitFor, and suggested write-back state. StepType is read from the database; Saury source is not parsed.
- Translation failures: untranslated Condition text, skipped control primitives, userTasks without participants, and degraded dataScope.
The BitzOrcas path also writes a report when output-dir is set, but the state map and hook list stay empty and the failure list only collects converter warnings.
Deploy and publish
Outside dry run the tool builds the engine on the SqlSugar Workflow store. DeployAsync(key, name, jsonDsl, "migrator", tenantId) creates an immutable version; the returned DefinitionId is passed to PublishDeploymentAsync. BitzOrcas still publishes with a null office. Saury passes each bound office as a string.
These two calls are not one atomic transaction owned by the migrator. A successful Deploy plus a failed Publish can leave an unpublished version. Query target versions and bindings before rerunning.
# After the isolated target is confirmed, turn DryRun off.dotnet run --configuration Release -- \ --source saury \ --output-dir /tmp/wf-saury \ > "$MIGRATION_EVIDENCE/workflow-deploy.log" 2>&1
rg -n 'Deploy|Publish|failed' \ "$MIGRATION_EVIDENCE/workflow-deploy.log"The tool prints Chinese status lines. Match those exact phrases from the zh page when grepping a real run.
Verify
- Every dry-run JSON deserializes and compiles through
DefinitionCompiler; seetests/BitzOrcas.Workflow.Migrator.Tests. - The sidecar has all three sections, and Hook StepType matches the database type name.
- The same input produces byte-identical JSON and reports on two runs.
- Invocations without
sourcestill use the BitzOrcas five-table path. - A WaitFor without permissions only warns; it does not invent assignees.
- Method-call conditions are not written onto edges.
- The target tenant can start a new instance from the published binding.
# Converter unit tests and Workflow-related architecture contracts.dotnet test tests/BitzOrcas.Workflow.Migrator.Tests \ --configuration Releasedotnet test tests/BitzOrcas.Architecture.Tests \ --configuration Release \ --filter 'FullyQualifiedName~Workflow'Roll back
- Keep dry-run logs, JSON, and sidecar files as approval artifacts.
- Unpublished definitions on the isolated target can be removed or disabled; do not blindly rerun against production.
- Published bindings roll back with
RollbackDeploymentAsynctoPreviousDefinitionId. Already started instances stay locked to the old snapshot. - The source remains a read-only snapshot; this tool never writes Saury or BitzOrcas legacy tables.
- Business callback code belongs to the business module and is not rolled back with the tool.
Exit codes
| Code | Current meaning | Automation |
|---|---|---|
| 0 | No flow threw; warnings may still exist | Continue semantic review; do not auto-promote |
| 1 | Missing connection strings | Block and fix configuration |
| 2 | At least one convert or deploy failed | Block and reconcile partial success |
| 3 | Ctrl+C / cancellation | Block and inspect partial deploy |
| 99 | Unhandled exception or source detection failure | Block and keep masked logs |
A single-flow failure does not stop the batch, so code 2 can mean partial success.
Delivery checklist
- Snapshot source and isolated target identities are traceable.
- The explicit or detected
sourceis recorded. - Every dry-run warning has an accept, fix, or block decision.
- Saury sidecar sections were handed to the business module for callback work.
- Definition key, version, Deploy, and Publish bindings were checked on the target.
- Instance migration is explicitly out of scope, with a cutover plan.
- Logs, JSON, and reports are stored as sensitive artifacts.