chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (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 / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env bun
/**
* CI check: enforces block-registry invariants that protect the runtime.
*
* Two checks run in sequence:
*
* 1. **Subblock ID stability** — diffs the current registry against a base ref
* and fails if any subblock ID was removed without a corresponding entry in
* `SUBBLOCK_ID_MIGRATIONS`. Removing IDs without a migration breaks
* deployed workflows.
*
* 2. **Canonical-id contract** — for every (block, tool) pair where the tool
* param is `required: true` and `visibility: 'user-only'`, the block must
* expose a subBlock whose `id` or `canonicalParamId` equals the tool param
* id. The serializer's pre-execution validator depends on this contract to
* resolve values via direct lookup; mismatches false-flag fields as missing
* at submit time.
*
* Usage:
* bun run apps/sim/scripts/check-block-registry.ts [base-ref]
*
* `base-ref` defaults to `HEAD~1`. In a PR CI pipeline, pass the merge base:
* bun run apps/sim/scripts/check-block-registry.ts origin/main
*/
import { execSync } from 'child_process'
import { SUBBLOCK_ID_MIGRATIONS } from '@/lib/workflows/migrations/subblock-migrations'
import { getAllBlocks, getBlockMeta } from '@/blocks/registry'
import { tools as toolRegistry } from '@/tools/registry'
const baseRef = process.argv[2] || 'HEAD~1'
const gitRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim()
const gitOpts = { encoding: 'utf-8' as const, cwd: gitRoot }
type IdMap = Record<string, Set<string>>
/**
* Extracts subblock IDs from the `subBlocks: [ ... ]` section of a block
* definition. Only grabs the top-level `id:` of each subblock object —
* ignores nested IDs inside `options`, `columns`, etc.
*/
function extractSubBlockIds(source: string): string[] {
const startIdx = source.indexOf('subBlocks:')
if (startIdx === -1) return []
const bracketStart = source.indexOf('[', startIdx)
if (bracketStart === -1) return []
const ids: string[] = []
let braceDepth = 0
let bracketDepth = 0
let i = bracketStart + 1
bracketDepth = 1
while (i < source.length && bracketDepth > 0) {
const ch = source[i]
if (ch === '[') bracketDepth++
else if (ch === ']') {
bracketDepth--
if (bracketDepth === 0) break
} else if (ch === '{') {
braceDepth++
if (braceDepth === 1) {
const ahead = source.slice(i, i + 200)
const idMatch = ahead.match(/{\s*(?:\/\/[^\n]*\n\s*)*id:\s*['"]([^'"]+)['"]/)
if (idMatch) {
ids.push(idMatch[1])
}
}
} else if (ch === '}') {
braceDepth--
}
i++
}
return ids
}
function getCurrentIds(): IdMap {
const map: IdMap = {}
for (const block of getAllBlocks()) {
map[block.type] = new Set(block.subBlocks.map((sb) => sb.id))
}
return map
}
type PreviousIdsResult =
| { kind: 'skip'; reason: string }
| { kind: 'noop' }
| { kind: 'ok'; map: IdMap }
function getPreviousIds(): PreviousIdsResult {
const registryPath = 'apps/sim/blocks/registry.ts'
const blocksDir = 'apps/sim/blocks/blocks'
let hasChanges = false
try {
const diff = execSync(
`git diff --name-only ${baseRef} HEAD -- ${registryPath} ${blocksDir}`,
gitOpts
).trim()
hasChanges = diff.length > 0
} catch {
return { kind: 'skip', reason: 'Could not diff against base ref' }
}
if (!hasChanges) {
return { kind: 'noop' }
}
const map: IdMap = {}
try {
const blockFiles = execSync(`git ls-tree -r --name-only ${baseRef} -- ${blocksDir}`, gitOpts)
.trim()
.split('\n')
.filter((f) => f.endsWith('.ts') && !f.endsWith('.test.ts'))
for (const filePath of blockFiles) {
let content: string
try {
content = execSync(`git show ${baseRef}:${filePath}`, gitOpts)
} catch {
continue
}
const typeMatch = content.match(/BlockConfig\s*=\s*\{[\s\S]*?type:\s*['"]([^'"]+)['"]/)
if (!typeMatch) continue
const blockType = typeMatch[1]
const ids = extractSubBlockIds(content)
if (ids.length === 0) continue
map[blockType] = new Set(ids)
}
} catch (err) {
return { kind: 'skip', reason: `Could not read previous block files from ${baseRef}: ${err}` }
}
return { kind: 'ok', map }
}
type CheckResult =
| { kind: 'pass'; message: string }
| { kind: 'skip'; message: string }
| { kind: 'fail'; errors: string[] }
function checkSubblockIdStability(): CheckResult {
const previous = getPreviousIds()
if (previous.kind === 'skip') {
return { kind: 'skip', message: `${previous.reason} — skipping subblock ID stability check` }
}
if (previous.kind === 'noop') {
return {
kind: 'skip',
message: 'No block definition changes detected — skipping subblock ID stability check',
}
}
const current = getCurrentIds()
const errors: string[] = []
for (const [blockType, prevIds] of Object.entries(previous.map)) {
const currIds = current[blockType]
if (!currIds) continue
const migrations = SUBBLOCK_ID_MIGRATIONS[blockType] ?? {}
for (const oldId of prevIds) {
if (currIds.has(oldId)) continue
if (oldId in migrations) continue
errors.push(
`Block "${blockType}": subblock ID "${oldId}" was removed.\n` +
` → Add a migration in SUBBLOCK_ID_MIGRATIONS (lib/workflows/migrations/subblock-migrations.ts)\n` +
` mapping "${oldId}" to its replacement ID.`
)
}
}
if (errors.length === 0) {
return { kind: 'pass', message: 'Subblock ID stability check passed' }
}
return { kind: 'fail', errors }
}
function checkCanonicalIdContract(): CheckResult {
const errors: string[] = []
for (const block of getAllBlocks()) {
const access: string[] = block.tools?.access ?? []
if (access.length === 0) continue
// A subBlock with `canonicalParamId` has its raw `id` deleted from `params` during
// canonical-group resolution in `extractParams` (serializer/index.ts), so the raw id is
// NOT a valid lookup key at execution time — only the canonical is. Tool params must
// align with the canonical, not the raw id.
const subBlockKeys = new Set<string>()
for (const sb of block.subBlocks ?? []) {
const canonical = (sb as { canonicalParamId?: string }).canonicalParamId
if (canonical) {
subBlockKeys.add(canonical)
} else if (sb.id) {
subBlockKeys.add(sb.id)
}
}
for (const toolId of access) {
const tool = toolRegistry[toolId]
if (!tool) continue
for (const [paramId, paramConfig] of Object.entries(tool.params ?? {})) {
if (!paramConfig || typeof paramConfig !== 'object') continue
const required = (paramConfig as { required?: boolean }).required === true
const userOnly = (paramConfig as { visibility?: string }).visibility === 'user-only'
if (!required || !userOnly) continue
if (!subBlockKeys.has(paramId)) {
errors.push(
`Block "${block.type}" → tool "${toolId}": required user-only param "${paramId}" has no subBlock with id or canonicalParamId === "${paramId}".\n` +
' → Rename a subBlock id or canonicalParamId to match the tool param id,\n' +
" and update the block's inputs + tools.config.params mapper to read from that key."
)
}
}
}
}
if (errors.length === 0) {
return { kind: 'pass', message: 'Canonical-id contract check passed' }
}
return { kind: 'fail', errors }
}
/**
* Every catalog-visible integration block must expose a `BlockMeta` entry in
* `BLOCK_META_REGISTRY`. The integration detail pages
* (`getTemplatesForBlock`/`getSuggestedSkillsForBlock`) and the landing catalog
* read metas by block type; a `tools`-category block that ships in the toolbar
* without a meta renders an empty detail page (no templates, no skills).
*
* "Catalog-visible integration" mirrors the `isIntegrationBlock` predicate in
* `scripts/generate-docs.ts`: `category === 'tools' && !hideFromToolbar`. Core
* primitives (`agent`/`api`/`function`/…), hidden/legacy base versions, and
* first-party `blocks`-category blocks intentionally carry no meta and are not
* checked. `getBlockMeta` resolves through the version suffix, so a versioned
* block (e.g. `github_v2`) passes via its base meta.
*/
function checkIntegrationMetaCoverage(): CheckResult {
const errors: string[] = []
for (const block of getAllBlocks()) {
// Unreleased preview blocks ship no BlockMeta until GA (they are absent
// from every catalog surface), so meta coverage must not force one. The
// registry projection already hides them here (no visibility context in a
// script), but the explicit check keeps this true regardless.
const isCatalogIntegration =
block.category === 'tools' && !block.hideFromToolbar && !block.preview
if (!isCatalogIntegration) continue
if (!getBlockMeta(block.type)) {
errors.push(
`Block "${block.type}" is a catalog integration (category: 'tools', not hidden) but has no BlockMeta.\n` +
` → Export a \`${block.type}\`-keyed \`{Service}BlockMeta\` (tags + templates + skills) from its block file\n` +
' and register it in BLOCK_META_REGISTRY (apps/sim/blocks/registry-maps.ts), alphabetically.'
)
}
}
if (errors.length === 0) {
return { kind: 'pass', message: 'Integration meta coverage check passed' }
}
return { kind: 'fail', errors }
}
function reportResult(label: string, failureHeader: string, result: CheckResult): boolean {
if (result.kind === 'pass') {
console.log(`${result.message}`)
return true
}
if (result.kind === 'skip') {
console.log(`${result.message}`)
return true
}
console.error(`\n✗ ${label} FAILED\n`)
if (failureHeader) console.error(`${failureHeader}\n`)
for (const err of result.errors) {
console.error(` ${err}\n`)
}
return false
}
const stabilityResult = checkSubblockIdStability()
const canonicalResult = checkCanonicalIdContract()
const metaCoverageResult = checkIntegrationMetaCoverage()
const stabilityOk = reportResult(
'Subblock ID stability check',
'Removing subblock IDs breaks deployed workflows.\nEither revert the rename or add a migration entry.',
stabilityResult
)
const canonicalOk = reportResult(
'Canonical-id contract check',
"Misaligned ids cause the serializer's pre-execution validator to false-flag fields as missing at submit time.",
canonicalResult
)
const metaCoverageOk = reportResult(
'Integration meta coverage check',
'Catalog integrations without a BlockMeta render empty detail pages (no templates, no skills).',
metaCoverageResult
)
process.exit(stabilityOk && canonicalOk && metaCoverageOk ? 0 : 1)