6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
import { getErrorMessage } from '@sim/utils/errors'
|
|
import { truncate } from '@sim/utils/string'
|
|
|
|
export const DEPLOYMENT_OPERATION_PROTOCOL_VERSION = 2
|
|
|
|
export const DEPLOYMENT_OPERATION_STATUSES = [
|
|
'preparing',
|
|
'activating',
|
|
'active',
|
|
'failed',
|
|
'superseded',
|
|
] as const
|
|
|
|
export const DEPLOYMENT_OPERATION_ACTIONS = ['deploy', 'activate'] as const
|
|
|
|
export const DEPLOYMENT_COMPONENT_STATUSES = ['pending', 'ready'] as const
|
|
|
|
export type DeploymentOperationStatus = (typeof DEPLOYMENT_OPERATION_STATUSES)[number]
|
|
export type DeploymentOperationAction = (typeof DEPLOYMENT_OPERATION_ACTIONS)[number]
|
|
export type DeploymentComponentStatus = (typeof DEPLOYMENT_COMPONENT_STATUSES)[number]
|
|
|
|
export interface SafeDeploymentError {
|
|
code: string
|
|
message: string
|
|
}
|
|
|
|
export interface DeploymentComponentReadiness {
|
|
status: DeploymentComponentStatus
|
|
updatedAt: string
|
|
}
|
|
|
|
export type DeploymentReadiness = Record<string, DeploymentComponentReadiness>
|
|
|
|
const ALLOWED_TRANSITIONS: Readonly<
|
|
Record<DeploymentOperationStatus, DeploymentOperationStatus[]>
|
|
> = {
|
|
preparing: ['activating', 'failed', 'superseded'],
|
|
activating: ['active', 'failed', 'superseded'],
|
|
active: [],
|
|
failed: [],
|
|
superseded: [],
|
|
}
|
|
|
|
const MAX_ERROR_CODE_LENGTH = 64
|
|
const MAX_ERROR_MESSAGE_LENGTH = 500
|
|
|
|
/**
|
|
* Narrows a persisted value to a supported operation status.
|
|
*/
|
|
export function isDeploymentOperationStatus(value: unknown): value is DeploymentOperationStatus {
|
|
return DEPLOYMENT_OPERATION_STATUSES.includes(value as DeploymentOperationStatus)
|
|
}
|
|
|
|
/**
|
|
* Narrows a persisted value to a supported operation action.
|
|
*/
|
|
export function isDeploymentOperationAction(value: unknown): value is DeploymentOperationAction {
|
|
return DEPLOYMENT_OPERATION_ACTIONS.includes(value as DeploymentOperationAction)
|
|
}
|
|
|
|
/**
|
|
* Returns whether an operation may move between two lifecycle states.
|
|
*/
|
|
export function canTransitionDeploymentOperation(
|
|
from: DeploymentOperationStatus,
|
|
to: DeploymentOperationStatus
|
|
): boolean {
|
|
return ALLOWED_TRANSITIONS[from].includes(to)
|
|
}
|
|
|
|
/**
|
|
* Builds the initial readiness map for the components required by an attempt.
|
|
*/
|
|
export function createDeploymentReadiness(
|
|
components: readonly string[],
|
|
updatedAt = new Date()
|
|
): DeploymentReadiness {
|
|
const readiness: DeploymentReadiness = {}
|
|
for (const component of components) {
|
|
const normalizedComponent = component.trim()
|
|
if (!normalizedComponent) {
|
|
throw new Error('Deployment readiness component names cannot be empty')
|
|
}
|
|
if (readiness[normalizedComponent]) {
|
|
throw new Error(`Duplicate deployment readiness component: ${normalizedComponent}`)
|
|
}
|
|
readiness[normalizedComponent] = {
|
|
status: 'pending',
|
|
updatedAt: updatedAt.toISOString(),
|
|
}
|
|
}
|
|
return readiness
|
|
}
|
|
|
|
/**
|
|
* Returns true only when every declared component is ready.
|
|
*/
|
|
export function isDeploymentReadinessComplete(readiness: DeploymentReadiness): boolean {
|
|
return Object.values(readiness).every((component) => component.status === 'ready')
|
|
}
|
|
|
|
/**
|
|
* Narrows persisted JSON to the lifecycle readiness shape.
|
|
*/
|
|
export function parseDeploymentReadiness(value: unknown): DeploymentReadiness | null {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return null
|
|
|
|
const readiness: DeploymentReadiness = {}
|
|
for (const [component, rawState] of Object.entries(value)) {
|
|
if (!rawState || typeof rawState !== 'object' || Array.isArray(rawState)) return null
|
|
|
|
const state = rawState as Record<string, unknown>
|
|
if (
|
|
!DEPLOYMENT_COMPONENT_STATUSES.includes(state.status as DeploymentComponentStatus) ||
|
|
typeof state.updatedAt !== 'string'
|
|
) {
|
|
return null
|
|
}
|
|
|
|
readiness[component] = {
|
|
status: state.status as DeploymentComponentStatus,
|
|
updatedAt: state.updatedAt,
|
|
}
|
|
}
|
|
|
|
return readiness
|
|
}
|
|
|
|
export const DEPLOYMENT_ERROR_CODES = {
|
|
webhookPathConflict: 'webhook_path_conflict',
|
|
invalidTriggerConfiguration: 'invalid_trigger_configuration',
|
|
} as const
|
|
|
|
const NON_RETRYABLE_DEPLOYMENT_ERROR_CODES = new Set<string>([
|
|
DEPLOYMENT_ERROR_CODES.webhookPathConflict,
|
|
DEPLOYMENT_ERROR_CODES.invalidTriggerConfiguration,
|
|
])
|
|
|
|
/**
|
|
* Returns true when a persisted deployment error code cannot succeed on a
|
|
* plain retry — the configuration must change first. Everything else
|
|
* (exhausted transient retries, generic failures) is worth redeploying as-is.
|
|
*/
|
|
export function isNonRetryableDeploymentErrorCode(code: string | null | undefined): boolean {
|
|
return code != null && NON_RETRYABLE_DEPLOYMENT_ERROR_CODES.has(code)
|
|
}
|
|
|
|
/**
|
|
* A preparation failure that cannot succeed on retry (path conflicts, invalid
|
|
* trigger configuration). The outbox handler fails the operation immediately
|
|
* instead of burning the full retry budget.
|
|
*/
|
|
export class NonRetryableDeploymentError extends Error {
|
|
constructor(
|
|
message: string,
|
|
readonly errorCode: string = 'preparation_failed'
|
|
) {
|
|
super(message)
|
|
this.name = 'NonRetryableDeploymentError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Narrows an unknown caught value to {@link NonRetryableDeploymentError}.
|
|
*/
|
|
export function isNonRetryableDeploymentError(
|
|
error: unknown
|
|
): error is NonRetryableDeploymentError {
|
|
return error instanceof NonRetryableDeploymentError
|
|
}
|
|
|
|
/**
|
|
* Converts an unknown failure into the bounded public shape persisted on an attempt.
|
|
*/
|
|
export function toSafeDeploymentError(error: unknown, errorCode?: string): SafeDeploymentError {
|
|
const source =
|
|
errorCode ??
|
|
(error && typeof error === 'object' && 'code' in error
|
|
? String((error as { code?: unknown }).code ?? 'deployment_failed')
|
|
: 'deployment_failed')
|
|
const code =
|
|
truncate(
|
|
source
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9_.-]+/g, '_'),
|
|
MAX_ERROR_CODE_LENGTH,
|
|
''
|
|
) || 'deployment_failed'
|
|
|
|
const message = sanitizeDeploymentErrorMessage(
|
|
getErrorMessage(error, 'Deployment operation failed')
|
|
)
|
|
return { code, message }
|
|
}
|
|
|
|
function sanitizeDeploymentErrorMessage(message: string): string {
|
|
/**
|
|
* Driver errors ("Failed query: ...\nparams: ...") embed every bound
|
|
* parameter value, which can include user credentials from provider
|
|
* configs — the parameter tail is dropped before anything else because the
|
|
* control-character pass below would otherwise fold it into one line.
|
|
*/
|
|
const withoutBoundParams = message.split(/\nparams: /)[0]
|
|
const withoutControlCharacters = withoutBoundParams.replace(/[\u0000-\u001F\u007F]+/g, ' ').trim()
|
|
const withoutUrlCredentials = withoutControlCharacters.replace(
|
|
/:\/\/[^/\s:@]+:[^/\s@]+@/g,
|
|
'://[redacted]@'
|
|
)
|
|
const withoutSecrets = withoutUrlCredentials.replace(
|
|
/\b(authorization|api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|secret|password)\b(\s*[:=]\s*)([^\s,;]+)/gi,
|
|
'$1$2[redacted]'
|
|
)
|
|
return truncate(withoutSecrets || 'Deployment operation failed', MAX_ERROR_MESSAGE_LENGTH, '')
|
|
}
|