Operations, leases, inventory, refresh, and result contracts
Use these operation kinds:
inventoryrefreshverificationUse these operation states:
reservedstartingstart_unknownrunningawaiting_reviewpublishingsucceededfailedrejectedexpiredA terminal state never changes. The terminal states are succeeded, failed, rejected, and
expired.
Each selected database has two independent result fields:
type RackspaceDatabaseOutcome = "changed" | "unchanged" | "failed";
type RackspaceDatabaseFreshness = "fresh" | "stale" | "unregistered";One failed database does not stop later database steps. The final report lists all selected databases and their results.
If a failed database has a merged snapshot, the package keeps its prior generated files and
registry entry, and the result has stale freshness. If a failed database has no merged snapshot,
the package does not register it, and the result has unregistered freshness.
Use one safe error type:
type RackspaceOperationError = {
stage: RackspaceErrorStage;
code: RackspaceErrorCode;
retryable: boolean;
message: string;
};Define fixed enums for stages and codes. Include authorization, lock, configuration, connection, TLS, permission, generation, metadata, verification, artifact, review, GitHub, and timeout stages.
Never store raw SQL, driver errors, credentials, connection URLs, or tokens in this type.
If at least one database changed, the pull-request description lists failures and stale snapshots. If no database changed, the workflow shows the report without a pull request.
The application database owns inventory, operation, review, and coordination state. Add these
tables through @concourse/db migrations:
| Table | Key | Purpose |
|---|---|---|
rackspaceSchemaInventory | id | Immutable inventory header |
rackspaceSchemaInventoryDatabase | inventoryId, databaseName | Ordered inventory names |
rackspaceSchemaOperation | id | Operation lifecycle |
rackspaceSchemaOperationDatabase | operationId, databaseName | Per-database result |
rackspaceSchemaOperationStep | operationId, stepKey | Idempotent progress |
rackspaceSchemaLease | Fixed key rackspace-schema-mutation | Refresh and verification exclusion |
rackspaceSchemaReview | operationId | Immutable review decision |
rackspaceSchemaArtifact | operationId | Encrypted artifact metadata |
rackspaceSchemaCleanupGrant | tokenHash | Short-lived cleanup authorization |
The inventory header stores its actor, server version, discovery time, and expiry. Its child rows store the stable order and exact database name.
The operation row stores kind, state, actor, inventory ID, Workflow run ID, reviewed base commit, pull-request data, safe error, and timestamps.
The database result stores outcome, freshness, object counts, hashes, timings, and a safe error. The row becomes immutable when its database work reaches a terminal state.
The step row uses a stable stepKey. It stores state, attempt count, heartbeat, safe error, and
terminal time. A retry reads this row before it repeats an external write.
The lease stores operation ID, kind, optional Workflow run ID, acquisition time, heartbeat, expiry, and status. The active lease expires 30 minutes after its latest heartbeat.
The review row stores pending, approved, rejected, or expired. It also stores the actor
and decision time. A conditional update selects one terminal decision.
The artifact row stores only the Blob locator, key version, initialization vector, authentication tag, ciphertext hash, canonical manifest, state, creation time, decision expiry, cleanup target, and cleanup result.
The cleanup-grant row stores a token hash, operation ID, actor, expiry, use time, and allowed cleanup types. The application never stores the plaintext token. A conditional update consumes it once.
The administrator page polls an authorized no-store route by operation ID. A terminal result row
is immutable and survives Workflow run retention.
Every external write uses a stable idempotency key. Each retry reads current state before it writes. This rule applies to Turso, Blob, hooks, Git refs, commits, and pull requests.
The global lease enforces these rules. Inventory does not use the lease:
The server action generates an operationId before it calls Workflow. For refresh and
verification, use this protocol:
systemAdmin session.reserved state.reserved to starting with a conditional update.start() once with the operation ID and safe identifiers.If the call returns a definite input error, mark the operation failed and release its lease. If
the result is uncertain, use start_unknown and keep the lease until expiry.
The action never calls start() again for the same operation ID. The first workflow step binds
its own run ID if the action lost the response. A repeated browser request returns the existing
operation. It does not create a second operation.
Inventory uses the same audit and operation protocol without the global lease.
Each step boundary updates step state and extends the lease heartbeat. Before review, the workflow extends the lease to the review expiry. A terminal workflow releases the lease with a conditional update.
The clear action accepts the exact operation ID as confirmation. It requires a fresh systemAdmin
session.
The action verifies that the lease expired. It also verifies that the Workflow run is terminal or unavailable.
One Turso transaction writes the audit event and marks the operation failed with LEASE_CLEARED.
The same transaction conditionally removes the matching lease. The action cannot remove a live
lease or a lease for another operation.
The inventory workflow is separate from snapshot generation. An administrator can inspect the available set before the administrator starts a long refresh.
The workflow uses the snapshot account to run SHOW DATABASES. MySQL returns only databases that
the account can see unless the account has broader global privileges.
The workflow always removes these system databases:
information_schemamysqlperformance_schemasysThe workflow sorts the remaining names with one stable rule. The application database stores an immutable inventory header and its ordered database names.
The inventory contains its operation ID, actor, engine version, discovery time, and expiry. An inventory expires 24 hours after discovery. A later refresh refers to the inventory by its ID.
The live inventory and the package registry have different purposes. The inventory lists visible databases. The registry lists only databases with successfully merged snapshots.
The administrator page provides these controls:
An empty selection is invalid. A selection can include every database without a special all-mode request.
Inventory remains available while a schema pull request is open. A new refresh remains blocked until that pull request is merged or closed.
The workflow receives an inventory ID and an ordered list of selected database names. It loads the inventory before it accesses Rackspace. It rejects an expired inventory.
Every selected name must match one inventory entry exactly. The workflow removes duplicate names and keeps inventory order.
Before each database step, the workflow verifies that the database remains visible. A database that is no longer visible receives a failed result.
The workflow never adds a database that the administrator did not select. A new Rackspace database requires a new inventory before it can be selected.
Each database runs in its own durable workflow step. The workflow awaits one result before it starts the next step.
Sequential work limits Rackspace load and makes progress clear. It also gives each database an independent retry and error result.
Each step uses a unique directory under the Vercel function /tmp directory. The step removes the
directory in a finally block after it creates or rejects its candidate.
A transient network or MySQL error receives no more than three attempts. The delays are 1 second, 3 seconds, and 9 seconds.
Authentication errors, permission errors, invalid identifiers, and unsupported schema output are permanent errors. The step records a sanitized result and does not retry those errors.
Each database step produces the candidate artifacts and verifies them with the shared connection verifier. Successful candidates enter one encrypted review artifact, and approved changes enter one GitHub pull request.
Operations, leases, inventory, refresh, and result contracts
Use these operation kinds:
inventoryrefreshverificationUse these operation states:
reservedstartingstart_unknownrunningawaiting_reviewpublishingsucceededfailedrejectedexpiredA terminal state never changes. The terminal states are succeeded, failed, rejected, and
expired.
Each selected database has two independent result fields:
type RackspaceDatabaseOutcome = "changed" | "unchanged" | "failed";
type RackspaceDatabaseFreshness = "fresh" | "stale" | "unregistered";One failed database does not stop later database steps. The final report lists all selected databases and their results.
If a failed database has a merged snapshot, the package keeps its prior generated files and
registry entry, and the result has stale freshness. If a failed database has no merged snapshot,
the package does not register it, and the result has unregistered freshness.
Use one safe error type:
type RackspaceOperationError = {
stage: RackspaceErrorStage;
code: RackspaceErrorCode;
retryable: boolean;
message: string;
};Define fixed enums for stages and codes. Include authorization, lock, configuration, connection, TLS, permission, generation, metadata, verification, artifact, review, GitHub, and timeout stages.
Never store raw SQL, driver errors, credentials, connection URLs, or tokens in this type.
If at least one database changed, the pull-request description lists failures and stale snapshots. If no database changed, the workflow shows the report without a pull request.
The application database owns inventory, operation, review, and coordination state. Add these
tables through @concourse/db migrations:
| Table | Key | Purpose |
|---|---|---|
rackspaceSchemaInventory | id | Immutable inventory header |
rackspaceSchemaInventoryDatabase | inventoryId, databaseName | Ordered inventory names |
rackspaceSchemaOperation | id | Operation lifecycle |
rackspaceSchemaOperationDatabase | operationId, databaseName | Per-database result |
rackspaceSchemaOperationStep | operationId, stepKey | Idempotent progress |
rackspaceSchemaLease | Fixed key rackspace-schema-mutation | Refresh and verification exclusion |
rackspaceSchemaReview | operationId | Immutable review decision |
rackspaceSchemaArtifact | operationId | Encrypted artifact metadata |
rackspaceSchemaCleanupGrant | tokenHash | Short-lived cleanup authorization |
The inventory header stores its actor, server version, discovery time, and expiry. Its child rows store the stable order and exact database name.
The operation row stores kind, state, actor, inventory ID, Workflow run ID, reviewed base commit, pull-request data, safe error, and timestamps.
The database result stores outcome, freshness, object counts, hashes, timings, and a safe error. The row becomes immutable when its database work reaches a terminal state.
The step row uses a stable stepKey. It stores state, attempt count, heartbeat, safe error, and
terminal time. A retry reads this row before it repeats an external write.
The lease stores operation ID, kind, optional Workflow run ID, acquisition time, heartbeat, expiry, and status. The active lease expires 30 minutes after its latest heartbeat.
The review row stores pending, approved, rejected, or expired. It also stores the actor
and decision time. A conditional update selects one terminal decision.
The artifact row stores only the Blob locator, key version, initialization vector, authentication tag, ciphertext hash, canonical manifest, state, creation time, decision expiry, cleanup target, and cleanup result.
The cleanup-grant row stores a token hash, operation ID, actor, expiry, use time, and allowed cleanup types. The application never stores the plaintext token. A conditional update consumes it once.
The administrator page polls an authorized no-store route by operation ID. A terminal result row
is immutable and survives Workflow run retention.
Every external write uses a stable idempotency key. Each retry reads current state before it writes. This rule applies to Turso, Blob, hooks, Git refs, commits, and pull requests.
The global lease enforces these rules. Inventory does not use the lease:
The server action generates an operationId before it calls Workflow. For refresh and
verification, use this protocol:
systemAdmin session.reserved state.reserved to starting with a conditional update.start() once with the operation ID and safe identifiers.If the call returns a definite input error, mark the operation failed and release its lease. If
the result is uncertain, use start_unknown and keep the lease until expiry.
The action never calls start() again for the same operation ID. The first workflow step binds
its own run ID if the action lost the response. A repeated browser request returns the existing
operation. It does not create a second operation.
Inventory uses the same audit and operation protocol without the global lease.
Each step boundary updates step state and extends the lease heartbeat. Before review, the workflow extends the lease to the review expiry. A terminal workflow releases the lease with a conditional update.
The clear action accepts the exact operation ID as confirmation. It requires a fresh systemAdmin
session.
The action verifies that the lease expired. It also verifies that the Workflow run is terminal or unavailable.
One Turso transaction writes the audit event and marks the operation failed with LEASE_CLEARED.
The same transaction conditionally removes the matching lease. The action cannot remove a live
lease or a lease for another operation.
The inventory workflow is separate from snapshot generation. An administrator can inspect the available set before the administrator starts a long refresh.
The workflow uses the snapshot account to run SHOW DATABASES. MySQL returns only databases that
the account can see unless the account has broader global privileges.
The workflow always removes these system databases:
information_schemamysqlperformance_schemasysThe workflow sorts the remaining names with one stable rule. The application database stores an immutable inventory header and its ordered database names.
The inventory contains its operation ID, actor, engine version, discovery time, and expiry. An inventory expires 24 hours after discovery. A later refresh refers to the inventory by its ID.
The live inventory and the package registry have different purposes. The inventory lists visible databases. The registry lists only databases with successfully merged snapshots.
The administrator page provides these controls:
An empty selection is invalid. A selection can include every database without a special all-mode request.
Inventory remains available while a schema pull request is open. A new refresh remains blocked until that pull request is merged or closed.
The workflow receives an inventory ID and an ordered list of selected database names. It loads the inventory before it accesses Rackspace. It rejects an expired inventory.
Every selected name must match one inventory entry exactly. The workflow removes duplicate names and keeps inventory order.
Before each database step, the workflow verifies that the database remains visible. A database that is no longer visible receives a failed result.
The workflow never adds a database that the administrator did not select. A new Rackspace database requires a new inventory before it can be selected.
Each database runs in its own durable workflow step. The workflow awaits one result before it starts the next step.
Sequential work limits Rackspace load and makes progress clear. It also gives each database an independent retry and error result.
Each step uses a unique directory under the Vercel function /tmp directory. The step removes the
directory in a finally block after it creates or rejects its candidate.
A transient network or MySQL error receives no more than three attempts. The delays are 1 second, 3 seconds, and 9 seconds.
Authentication errors, permission errors, invalid identifiers, and unsupported schema output are permanent errors. The step records a sanitized result and does not retry those errors.
Each database step produces the candidate artifacts and verifies them with the shared connection verifier. Successful candidates enter one encrypted review artifact, and approved changes enter one GitHub pull request.