thinkwork-cli 0.13.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -14
- package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
- package/dist/bowser-6Z6JFAIR.js +2822 -0
- package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
- package/dist/chunk-AD2NIWB5.js +6516 -0
- package/dist/chunk-DV3MRBR5.js +43 -0
- package/dist/chunk-EI5ZX7CN.js +522 -0
- package/dist/chunk-EUJ7X6YD.js +126 -0
- package/dist/chunk-GG5WKBOX.js +436 -0
- package/dist/chunk-I64EZSU5.js +358 -0
- package/dist/chunk-KMIJ5KGT.js +128 -0
- package/dist/chunk-MWQ7RO76.js +137 -0
- package/dist/chunk-OCPHU34C.js +996 -0
- package/dist/chunk-OLUQ4U2Z.js +1728 -0
- package/dist/chunk-XYM7PE2A.js +2264 -0
- package/dist/chunk-YQCYD6NE.js +3299 -0
- package/dist/cli.js +49156 -31257
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
- package/dist/dist-es-2OTNTL4N.js +315 -0
- package/dist/dist-es-2XVAH4BQ.js +14726 -0
- package/dist/dist-es-3DPDUJAH.js +2000 -0
- package/dist/dist-es-BNZTBBVC.js +176 -0
- package/dist/dist-es-CD2RBDAC.js +88 -0
- package/dist/dist-es-I4FW57PS.js +88 -0
- package/dist/dist-es-KL7Y5NB2.js +377 -0
- package/dist/dist-es-KVFWJNDY.js +318 -0
- package/dist/dist-es-PS73DWNL.js +70 -0
- package/dist/dist-es-S3NZQFGT.js +170 -0
- package/dist/dist-es-SGCA4P32.js +48 -0
- package/dist/dist-es-WBM6J4RA.js +70 -0
- package/dist/dist-es-ZGVADYIF.js +485 -0
- package/dist/dist-es-ZKC2QKD2.js +377 -0
- package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
- package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
- package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
- package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
- package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
- package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
- package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
- package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
- package/dist/drizzle/0230_analyst_rls.sql +28 -36
- package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
- package/dist/drizzle/0251_brain_stragglers.sql +21 -0
- package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
- package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
- package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
- package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
- package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
- package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
- package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
- package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
- package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
- package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
- package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
- package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
- package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
- package/dist/drizzle/0269_ontology_system_map.sql +31 -0
- package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
- package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
- package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
- package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
- package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
- package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
- package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
- package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
- package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
- package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
- package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
- package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
- package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
- package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
- package/dist/drizzle/0284_user_brain_claims.sql +69 -0
- package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
- package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
- package/dist/drizzle/0287_brain_subject.sql +25 -0
- package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
- package/dist/event-streams-7JNLJEEL.js +894 -0
- package/dist/event-streams-KDF5RWR6.js +1515 -0
- package/dist/loadSso-ETCXTR4I.js +576 -0
- package/dist/loadSso-UBD7KUHE.js +583 -0
- package/dist/plugins/README.md +2 -2
- package/dist/plugins/catalog/package.json +1 -5
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
- package/dist/plugins/catalog/src/contracts.ts +17 -179
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
- package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
- package/dist/plugins/twenty/package.json +2 -1
- package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
- package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
- package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
- package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
- package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
- package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
- package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
- package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
- package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
- package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
- package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
- package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
- package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
- package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
- package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
- package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
- package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
- package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
- package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
- package/dist/plugins/twenty/test/manifest.test.ts +2 -4
- package/dist/scripts/post-deploy.sh +79 -1
- package/dist/signin-5QZNRSOO.js +821 -0
- package/dist/sso-oidc-XK673P54.js +837 -0
- package/dist/sts-OXI2UALF.js +1113 -0
- package/dist/sts-SQW4NBK3.js +1117 -0
- package/dist/terraform/examples/greenfield/main.tf +391 -176
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
- package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
- package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
- package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
- package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
- package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
- package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
- package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
- package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
- package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
- package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
- package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
- package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
- package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
- package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
- package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
- package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
- package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
- package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
- package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
- package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
- package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
- package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
- package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
- package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
- package/dist/terraform/modules/app/static-site/main.tf +1 -1
- package/dist/terraform/modules/app/www-dns/main.tf +9 -27
- package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
- package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
- package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
- package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
- package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
- package/dist/terraform/modules/thinkwork/README.md +5 -1
- package/dist/terraform/modules/thinkwork/main.tf +459 -234
- package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
- package/dist/terraform/modules/thinkwork/variables.tf +353 -89
- package/dist/terraform/schema.graphql +69 -36
- package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
- package/dist/workspace-defaults/files/ROUTER.md +1 -1
- package/dist/workspace-defaults/files/TOOLS.md +9 -39
- package/dist/workspace-defaults/files/USER.md +1 -0
- package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
- package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
- package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
- package/package.json +3 -2
- package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
- package/dist/plugins/company-data/README.md +0 -49
- package/dist/plugins/company-data/package.json +0 -21
- package/dist/plugins/company-data/src/index.ts +0 -30
- package/dist/plugins/company-data/src/manifest.ts +0 -26
- package/dist/plugins/company-data/test/manifest.test.ts +0 -116
- package/dist/plugins/company-data/tsconfig.json +0 -8
- package/dist/plugins/company-etl/README.md +0 -46
- package/dist/plugins/company-etl/package.json +0 -21
- package/dist/plugins/company-etl/src/index.ts +0 -29
- package/dist/plugins/company-etl/src/manifest.ts +0 -26
- package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
- package/dist/plugins/company-etl/tsconfig.json +0 -8
- package/dist/plugins/lastmile/README.md +0 -50
- package/dist/plugins/lastmile/package.json +0 -23
- package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
- package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
- package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
- package/dist/plugins/lastmile/src/index.ts +0 -46
- package/dist/plugins/lastmile/src/manifest.ts +0 -139
- package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
- package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
- package/dist/plugins/lastmile/tsconfig.json +0 -8
- package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
- package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
- package/dist/plugins/workos-auth/package.json +0 -22
- package/dist/plugins/workos-auth/src/index.ts +0 -30
- package/dist/plugins/workos-auth/src/manifest.ts +0 -40
- package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
- package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
- package/dist/plugins/workos-auth/tsconfig.json +0 -8
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
- package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
- package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
- package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
- package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
- package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* One-off: strip the `LM_` prefix from opportunity stage VALUES, and remove the
|
|
4
|
+
* stage options no record uses.
|
|
5
|
+
*
|
|
6
|
+
* The first import wrote values like `LM_10_PROSPECT`. Labels were always clean
|
|
7
|
+
* ("10-Prospect"), but the raw value surfaces in the API, in filters, and in
|
|
8
|
+
* generated reports. It also left 8 options behind from an abandoned model.
|
|
9
|
+
*
|
|
10
|
+
* Order matters. Deleting an option while records still reference its value
|
|
11
|
+
* strands those records on a value the field no longer offers, so this runs in
|
|
12
|
+
* three phases:
|
|
13
|
+
*
|
|
14
|
+
* 1. ADD — write options = existing ∪ clean values. Both old and new exist.
|
|
15
|
+
* 2. RETAG — move every record from its LM_ value to the clean one.
|
|
16
|
+
* 3. PRUNE — write options = final set, dropping LM_* and the 8 orphans.
|
|
17
|
+
*
|
|
18
|
+
* A crash between phases is safe: phase 1 is idempotent, phase 2 re-runs over
|
|
19
|
+
* whatever is left, and phase 3 refuses to drop a value that still has records.
|
|
20
|
+
*
|
|
21
|
+
* CUSTOMER is never dropped — the ThinkWork workflow triggers on it.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* npx tsx scripts/retag-stage-values.ts # dry-run: show the plan
|
|
25
|
+
* npx tsx scripts/retag-stage-values.ts --apply
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import process from "node:process";
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
LEGACY_STAGE_VALUE_MAP,
|
|
32
|
+
MIGRATION_STAGE_OPTIONS,
|
|
33
|
+
OBSOLETE_STAGE_VALUES,
|
|
34
|
+
} from "./lib/mappers";
|
|
35
|
+
import { chunk, TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
|
|
36
|
+
|
|
37
|
+
interface StageOption {
|
|
38
|
+
id?: string;
|
|
39
|
+
label: string;
|
|
40
|
+
value: string;
|
|
41
|
+
color: string;
|
|
42
|
+
position: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const UPDATE_BATCH = 60;
|
|
46
|
+
|
|
47
|
+
function requireEnv(name: string): string {
|
|
48
|
+
const value = process.env[name];
|
|
49
|
+
if (!value) throw new Error(`Missing required environment variable: ${name}`);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function log(message: string): void {
|
|
54
|
+
process.stderr.write(`${message}\n`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readStageField(
|
|
58
|
+
client: TwentyClient,
|
|
59
|
+
): Promise<{ id: string; options: StageOption[] }> {
|
|
60
|
+
const data = await client.requestWithRetry<{
|
|
61
|
+
objects: {
|
|
62
|
+
edges: Array<{
|
|
63
|
+
node: {
|
|
64
|
+
nameSingular: string;
|
|
65
|
+
fields: {
|
|
66
|
+
edges: Array<{
|
|
67
|
+
node: { id: string; name: string; options: StageOption[] | null };
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
}>(
|
|
74
|
+
"/metadata",
|
|
75
|
+
`query RetagStageField {
|
|
76
|
+
objects(paging: { first: 1000 }) {
|
|
77
|
+
edges { node { nameSingular fields(paging: { first: 1000 }) { edges { node { id name options } } } } }
|
|
78
|
+
}
|
|
79
|
+
}`,
|
|
80
|
+
);
|
|
81
|
+
const opportunity = data.objects.edges.find(
|
|
82
|
+
(edge) => edge.node.nameSingular === "opportunity",
|
|
83
|
+
);
|
|
84
|
+
const stage = opportunity?.node.fields.edges.find(
|
|
85
|
+
(edge) => edge.node.name === "stage",
|
|
86
|
+
)?.node;
|
|
87
|
+
if (!stage?.options) throw new Error("opportunity.stage field not found.");
|
|
88
|
+
return { id: stage.id, options: stage.options };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function writeOptions(
|
|
92
|
+
client: TwentyClient,
|
|
93
|
+
fieldId: string,
|
|
94
|
+
options: StageOption[],
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
await client.requestOnce(
|
|
97
|
+
"/metadata",
|
|
98
|
+
`mutation RetagWriteOptions($input: UpdateOneFieldMetadataInput!) {
|
|
99
|
+
updateOneField(input: $input) { id }
|
|
100
|
+
}`,
|
|
101
|
+
{ input: { id: fieldId, update: { options } } },
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Confirm a freshly-added option value is actually live before any record is
|
|
107
|
+
* moved onto it. Twenty accepts the metadata write immediately, but the
|
|
108
|
+
* underlying Postgres enum lags — and a record written to a not-yet-live value
|
|
109
|
+
* is silently coerced to the field's default rather than rejected. That cost
|
|
110
|
+
* 1,510 opportunities their stage on 2026-07-10; they were rebuilt from
|
|
111
|
+
* LastMile, but the write must never be attempted blind again.
|
|
112
|
+
*/
|
|
113
|
+
async function waitForOptionsLive(
|
|
114
|
+
client: TwentyClient,
|
|
115
|
+
values: readonly string[],
|
|
116
|
+
attempts = 10,
|
|
117
|
+
): Promise<void> {
|
|
118
|
+
for (const value of values) {
|
|
119
|
+
let live = false;
|
|
120
|
+
for (let attempt = 0; attempt < attempts && !live; attempt += 1) {
|
|
121
|
+
try {
|
|
122
|
+
await client.requestWithRetry(
|
|
123
|
+
"/graphql",
|
|
124
|
+
`query RetagProbe($filter: OpportunityFilterInput) {
|
|
125
|
+
opportunities(filter: $filter, first: 1) { edges { node { id } } }
|
|
126
|
+
}`,
|
|
127
|
+
{ filter: { stage: { eq: value } } },
|
|
128
|
+
);
|
|
129
|
+
live = true;
|
|
130
|
+
} catch {
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!live) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Stage option "${value}" never became queryable; refusing to retag records onto it.`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Count records per stage value so PRUNE can refuse to strand any. */
|
|
143
|
+
async function countByStage(
|
|
144
|
+
client: TwentyClient,
|
|
145
|
+
): Promise<Map<string, number>> {
|
|
146
|
+
const counts = new Map<string, number>();
|
|
147
|
+
let after: string | null = null;
|
|
148
|
+
for (;;) {
|
|
149
|
+
const data: {
|
|
150
|
+
opportunities: {
|
|
151
|
+
edges: Array<{ node: { id: string; stage: string | null } }>;
|
|
152
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
153
|
+
};
|
|
154
|
+
} = await client.requestWithRetry(
|
|
155
|
+
"/graphql",
|
|
156
|
+
`query RetagCount($after: String) {
|
|
157
|
+
opportunities(first: 100, after: $after) {
|
|
158
|
+
edges { node { id stage } cursor }
|
|
159
|
+
pageInfo { hasNextPage endCursor }
|
|
160
|
+
}
|
|
161
|
+
}`,
|
|
162
|
+
after ? { after } : {},
|
|
163
|
+
);
|
|
164
|
+
for (const { node } of data.opportunities.edges) {
|
|
165
|
+
const key = node.stage ?? "NULL";
|
|
166
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
167
|
+
}
|
|
168
|
+
if (!data.opportunities.pageInfo.hasNextPage) break;
|
|
169
|
+
after = data.opportunities.pageInfo.endCursor;
|
|
170
|
+
}
|
|
171
|
+
return counts;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function idsForStage(
|
|
175
|
+
client: TwentyClient,
|
|
176
|
+
stage: string,
|
|
177
|
+
): Promise<string[]> {
|
|
178
|
+
const ids: string[] = [];
|
|
179
|
+
let after: string | null = null;
|
|
180
|
+
for (;;) {
|
|
181
|
+
const data: {
|
|
182
|
+
opportunities: {
|
|
183
|
+
edges: Array<{ node: { id: string } }>;
|
|
184
|
+
pageInfo: { hasNextPage: boolean; endCursor: string | null };
|
|
185
|
+
};
|
|
186
|
+
} = await client.requestWithRetry(
|
|
187
|
+
"/graphql",
|
|
188
|
+
`query RetagIds($filter: OpportunityFilterInput, $after: String) {
|
|
189
|
+
opportunities(filter: $filter, first: 100, after: $after) {
|
|
190
|
+
edges { node { id } cursor }
|
|
191
|
+
pageInfo { hasNextPage endCursor }
|
|
192
|
+
}
|
|
193
|
+
}`,
|
|
194
|
+
{ filter: { stage: { eq: stage } }, ...(after ? { after } : {}) },
|
|
195
|
+
);
|
|
196
|
+
ids.push(...data.opportunities.edges.map((edge) => edge.node.id));
|
|
197
|
+
if (!data.opportunities.pageInfo.hasNextPage) break;
|
|
198
|
+
after = data.opportunities.pageInfo.endCursor;
|
|
199
|
+
}
|
|
200
|
+
return ids;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function main(): Promise<void> {
|
|
204
|
+
const apply = process.argv.slice(2).includes("--apply");
|
|
205
|
+
const client = new TwentyClient({
|
|
206
|
+
baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
|
|
207
|
+
authToken: requireEnv("TWENTY_API_KEY"),
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const { id: fieldId, options } = await readStageField(client);
|
|
211
|
+
const before = await countByStage(client);
|
|
212
|
+
const report: Record<string, unknown> = {
|
|
213
|
+
mode: apply ? "apply" : "dry-run",
|
|
214
|
+
stageCountsBefore: Object.fromEntries(before),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const cleanValues = new Set(MIGRATION_STAGE_OPTIONS.map((o) => o.value));
|
|
218
|
+
const retagged = Object.entries(LEGACY_STAGE_VALUE_MAP).filter(
|
|
219
|
+
([from]) => (before.get(from) ?? 0) > 0,
|
|
220
|
+
);
|
|
221
|
+
report.willRetag = Object.fromEntries(
|
|
222
|
+
retagged.map(([from, to]) => [from, `${to} (${before.get(from)} records)`]),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// --- Phase 1: ADD -------------------------------------------------------
|
|
226
|
+
const existingValues = new Set(options.map((option) => option.value));
|
|
227
|
+
const additions = MIGRATION_STAGE_OPTIONS.filter(
|
|
228
|
+
(option) => !existingValues.has(option.value),
|
|
229
|
+
);
|
|
230
|
+
const migrationByValue = new Map(
|
|
231
|
+
MIGRATION_STAGE_OPTIONS.map((option) => [option.value, option]),
|
|
232
|
+
);
|
|
233
|
+
// A reused option (NEW, PROSPECT, WON, LOST, FORMULATE_OFFER) must take
|
|
234
|
+
// LastMile's label and colour, or the picker shows "Won" where the source
|
|
235
|
+
// says "60-Won".
|
|
236
|
+
const relabelled: string[] = [];
|
|
237
|
+
const phase1: StageOption[] = [
|
|
238
|
+
...options.map((option, index) => {
|
|
239
|
+
const replacement = migrationByValue.get(option.value);
|
|
240
|
+
if (!replacement) return { ...option, position: index };
|
|
241
|
+
if (option.label !== replacement.label) relabelled.push(option.value);
|
|
242
|
+
return {
|
|
243
|
+
...option,
|
|
244
|
+
label: replacement.label,
|
|
245
|
+
color: replacement.color,
|
|
246
|
+
position: index,
|
|
247
|
+
};
|
|
248
|
+
}),
|
|
249
|
+
...additions.map((option, index) => ({
|
|
250
|
+
label: option.label,
|
|
251
|
+
value: option.value,
|
|
252
|
+
color: option.color,
|
|
253
|
+
position: options.length + index,
|
|
254
|
+
})),
|
|
255
|
+
];
|
|
256
|
+
report.phase1Add = additions.map((option) => option.value);
|
|
257
|
+
report.phase1Relabel = relabelled;
|
|
258
|
+
|
|
259
|
+
if (apply && (additions.length > 0 || relabelled.length > 0)) {
|
|
260
|
+
log(
|
|
261
|
+
`phase 1: adding ${additions.length} options, relabelling ${relabelled.length}...`,
|
|
262
|
+
);
|
|
263
|
+
await writeOptions(client, fieldId, phase1);
|
|
264
|
+
// The enum lags the metadata write; moving records onto a value that is not
|
|
265
|
+
// yet live silently coerces them to the default.
|
|
266
|
+
log("phase 1: waiting for new options to go live...");
|
|
267
|
+
await waitForOptionsLive(
|
|
268
|
+
client,
|
|
269
|
+
additions.map((option) => option.value),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// --- Phase 2: RETAG -----------------------------------------------------
|
|
274
|
+
const moved: Record<string, number> = {};
|
|
275
|
+
if (apply) {
|
|
276
|
+
for (const [from, to] of retagged) {
|
|
277
|
+
const ids = await idsForStage(client, from);
|
|
278
|
+
log(`phase 2: ${from} -> ${to} (${ids.length} records)`);
|
|
279
|
+
for (const batch of chunk(ids, UPDATE_BATCH)) {
|
|
280
|
+
await client.requestOnce(
|
|
281
|
+
"/graphql",
|
|
282
|
+
`mutation RetagMove($filter: OpportunityFilterInput!, $data: OpportunityUpdateInput!) {
|
|
283
|
+
updateOpportunities(filter: $filter, data: $data) { id }
|
|
284
|
+
}`,
|
|
285
|
+
{ filter: { id: { in: batch } }, data: { stage: to } },
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
moved[from] = ids.length;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
report.phase2Moved = apply ? moved : "(dry-run)";
|
|
292
|
+
|
|
293
|
+
// --- Phase 3: PRUNE -----------------------------------------------------
|
|
294
|
+
const after = apply ? await countByStage(client) : before;
|
|
295
|
+
|
|
296
|
+
// Every record must have landed on the value we aimed it at. Verify BEFORE
|
|
297
|
+
// pruning, because pruning is the destructive step and a silent coercion
|
|
298
|
+
// would otherwise be locked in.
|
|
299
|
+
if (apply) {
|
|
300
|
+
const expected = new Map<string, number>();
|
|
301
|
+
for (const [from, to] of Object.entries(LEGACY_STAGE_VALUE_MAP)) {
|
|
302
|
+
const n = before.get(from) ?? 0;
|
|
303
|
+
if (n > 0) expected.set(to, (expected.get(to) ?? 0) + n);
|
|
304
|
+
}
|
|
305
|
+
const drift = [...expected.entries()].filter(
|
|
306
|
+
([value, count]) => (after.get(value) ?? 0) !== count,
|
|
307
|
+
);
|
|
308
|
+
if (drift.length > 0) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
"Retag did not land as planned — refusing to prune. " +
|
|
311
|
+
drift
|
|
312
|
+
.map(
|
|
313
|
+
([value, want]) =>
|
|
314
|
+
`${value}: expected ${want}, found ${after.get(value) ?? 0}`,
|
|
315
|
+
)
|
|
316
|
+
.join("; ") +
|
|
317
|
+
". Re-run migrate-lastmile.ts --apply to rebuild stages from LastMile.",
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const dropCandidates = [
|
|
322
|
+
...Object.keys(LEGACY_STAGE_VALUE_MAP),
|
|
323
|
+
...OBSOLETE_STAGE_VALUES,
|
|
324
|
+
];
|
|
325
|
+
const stillUsed = dropCandidates.filter(
|
|
326
|
+
(value) => (after.get(value) ?? 0) > 0,
|
|
327
|
+
);
|
|
328
|
+
if (stillUsed.length > 0 && apply) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Refusing to prune stage options still referenced by records: ${stillUsed.join(", ")}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const keep = phase1.filter(
|
|
334
|
+
(option) =>
|
|
335
|
+
!dropCandidates.includes(option.value) || cleanValues.has(option.value),
|
|
336
|
+
);
|
|
337
|
+
report.phase3Prune = dropCandidates.filter(
|
|
338
|
+
(value) => !cleanValues.has(value) && existingValues.has(value),
|
|
339
|
+
);
|
|
340
|
+
report.finalOptions = keep.map((option) => option.value);
|
|
341
|
+
|
|
342
|
+
if (apply) {
|
|
343
|
+
log(
|
|
344
|
+
`phase 3: pruning ${(report.phase3Prune as string[]).length} options...`,
|
|
345
|
+
);
|
|
346
|
+
await writeOptions(
|
|
347
|
+
client,
|
|
348
|
+
fieldId,
|
|
349
|
+
keep.map((option, index) => ({ ...option, position: index })),
|
|
350
|
+
);
|
|
351
|
+
report.stageCountsAfter = Object.fromEntries(await countByStage(client));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
main().catch((error) => {
|
|
358
|
+
process.stderr.write(
|
|
359
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
360
|
+
);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
|
@@ -359,10 +359,8 @@ describe("twenty plugin manifest", () => {
|
|
|
359
359
|
});
|
|
360
360
|
|
|
361
361
|
it("enables Twenty local logic functions for the first-party workflow action", () => {
|
|
362
|
-
const terraformModule = readRepo("
|
|
363
|
-
const terraformReadme = readRepo(
|
|
364
|
-
"plugins/twenty/terraform/twenty/README.md",
|
|
365
|
-
);
|
|
362
|
+
const terraformModule = readRepo("terraform/modules/app/twenty/main.tf");
|
|
363
|
+
const terraformReadme = readRepo("terraform/modules/app/twenty/README.md");
|
|
366
364
|
|
|
367
365
|
expect(terraformModule).toContain(
|
|
368
366
|
'{ name = "LOGIC_FUNCTION_TYPE", value = "LOCAL" }',
|
|
@@ -30,6 +30,7 @@ STRICT=0
|
|
|
30
30
|
JSON=0
|
|
31
31
|
REGION="${AWS_REGION:-us-east-1}"
|
|
32
32
|
MIN_SOURCE_SHA=""
|
|
33
|
+
EXPECTED_DIGEST=""
|
|
33
34
|
RUNTIME="pi"
|
|
34
35
|
AGENTCORE_CONTROL_TIMEOUT_SECONDS="${AGENTCORE_CONTROL_TIMEOUT_SECONDS:-20}"
|
|
35
36
|
|
|
@@ -41,6 +42,7 @@ while [[ $# -gt 0 ]]; do
|
|
|
41
42
|
--region) REGION="${2:?}"; shift 2 ;;
|
|
42
43
|
--runtime) RUNTIME="${2:?}"; shift 2 ;;
|
|
43
44
|
--min-source-sha) MIN_SOURCE_SHA="${2:?}"; shift 2 ;;
|
|
45
|
+
--expected-digest) EXPECTED_DIGEST="${2:?}"; shift 2 ;;
|
|
44
46
|
--help|-h)
|
|
45
47
|
sed -n '3,30p' "$0"
|
|
46
48
|
exit 0
|
|
@@ -57,6 +59,11 @@ if [[ -z "$STAGE" ]]; then
|
|
|
57
59
|
exit 2
|
|
58
60
|
fi
|
|
59
61
|
|
|
62
|
+
if [[ -n "$EXPECTED_DIGEST" && ! "$EXPECTED_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
|
63
|
+
echo "ERROR: --expected-digest must be sha256:<64 hex chars> (got '$EXPECTED_DIGEST')" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
|
|
60
67
|
case "$RUNTIME" in
|
|
61
68
|
pi) ;;
|
|
62
69
|
strands)
|
|
@@ -91,12 +98,61 @@ log() {
|
|
|
91
98
|
|
|
92
99
|
image_sha_from_uri() {
|
|
93
100
|
local image_uri="$1"
|
|
101
|
+
# Digest-pinned URIs (repo@sha256:...) carry no tag; without this guard the
|
|
102
|
+
# first 40 hex chars of the digest would masquerade as a source commit sha.
|
|
103
|
+
if [[ "$image_uri" == *"@sha256:"* ]]; then
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
94
106
|
local tag="${image_uri##*:}"
|
|
95
107
|
if [[ "$tag" =~ ([0-9a-f]{40}) ]]; then
|
|
96
108
|
echo "${BASH_REMATCH[1]}"
|
|
97
109
|
fi
|
|
98
110
|
}
|
|
99
111
|
|
|
112
|
+
digest_from_uri() {
|
|
113
|
+
local image_uri="$1"
|
|
114
|
+
if [[ "$image_uri" =~ @(sha256:[0-9a-f]{64})$ ]]; then
|
|
115
|
+
echo "${BASH_REMATCH[1]}"
|
|
116
|
+
fi
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
source_sha_from_digest() {
|
|
120
|
+
# A digest-pinned image URI carries no tag, but the image still has its
|
|
121
|
+
# build tags in ECR — recover the 40-hex source sha from them so the
|
|
122
|
+
# ancestry check works for digest-pinned runtimes. Two builds of the same
|
|
123
|
+
# source produce different digests, so digest EQUALITY against a
|
|
124
|
+
# source-sha-derived tag is the wrong invariant (2026-08-04 deploy
|
|
125
|
+
# 30872795807); tag-recovered ancestry is the right one.
|
|
126
|
+
local image_uri="$1" digest="$2"
|
|
127
|
+
local repo_path="${image_uri%%@*}"
|
|
128
|
+
local repo_name="${repo_path##*/}"
|
|
129
|
+
local tags tag errfile
|
|
130
|
+
# Keep stdout (the recovered sha) separate from any describe-images error so
|
|
131
|
+
# a failed recovery is diagnosable on stderr instead of collapsing to a bare
|
|
132
|
+
# `sha=unknown` — that opacity is what made THINK-1119 hard to trace. Callers
|
|
133
|
+
# that know the exact pinned digest should pass --expected-digest, which
|
|
134
|
+
# skips this ECR-tag round-trip entirely.
|
|
135
|
+
errfile=$(mktemp)
|
|
136
|
+
if ! tags=$(aws ecr describe-images \
|
|
137
|
+
--repository-name "$repo_name" \
|
|
138
|
+
--region "$REGION" \
|
|
139
|
+
--image-ids "imageDigest=$digest" \
|
|
140
|
+
--query 'imageDetails[0].imageTags' \
|
|
141
|
+
--output json 2>"$errfile"); then
|
|
142
|
+
echo "WARN: ecr describe-images for ${repo_name}@${digest} failed: $(tr '\n' ' ' <"$errfile")" >&2
|
|
143
|
+
rm -f "$errfile"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
rm -f "$errfile"
|
|
147
|
+
while IFS= read -r tag; do
|
|
148
|
+
if [[ "$tag" =~ ([0-9a-f]{40}) ]]; then
|
|
149
|
+
echo "${BASH_REMATCH[1]}"
|
|
150
|
+
return 0
|
|
151
|
+
fi
|
|
152
|
+
done < <(echo "$tags" | jq -r '.[]? // empty')
|
|
153
|
+
echo "WARN: no 40-hex source-sha tag on ${repo_name}@${digest} (tags=$(echo "$tags" | jq -c '. // []' 2>/dev/null)); pass --expected-digest to assert provenance directly" >&2
|
|
154
|
+
}
|
|
155
|
+
|
|
100
156
|
image_contains_source_sha() {
|
|
101
157
|
local image_sha="$1"
|
|
102
158
|
local source_sha="$2"
|
|
@@ -200,6 +256,7 @@ while read -r rt_id rt_name rt_status; do
|
|
|
200
256
|
rt_version=$(echo "$rt_detail" | jq -r '.agentRuntimeVersion // "null"')
|
|
201
257
|
rt_image=$(echo "$rt_detail" | jq -r '.agentRuntimeArtifact.containerConfiguration.containerUri // ""')
|
|
202
258
|
rt_image_sha=$(image_sha_from_uri "$rt_image")
|
|
259
|
+
rt_digest=$(digest_from_uri "$rt_image")
|
|
203
260
|
|
|
204
261
|
# DEFAULT endpoint is the one Terraform manages for us
|
|
205
262
|
eps=$(agentcore_control list-agent-runtime-endpoints \
|
|
@@ -248,7 +305,28 @@ while read -r rt_id rt_name rt_status; do
|
|
|
248
305
|
is_clean=0
|
|
249
306
|
reasons+=("endpoint liveVersion=$ep_live != runtime version=$rt_version")
|
|
250
307
|
fi
|
|
251
|
-
|
|
308
|
+
# Digest pinning (THINK-584 U5): --expected-digest is for callers that
|
|
309
|
+
# know the EXACT digest the runtime was just pinned to (the control-plane
|
|
310
|
+
# runner). When it's set and the runtime serves a digest-pinned image,
|
|
311
|
+
# they must match. A legacy TAG-form runtime image is not a digest
|
|
312
|
+
# mismatch — it falls through to the ancestry check, so the first
|
|
313
|
+
# post-merge deploy (runtime not yet re-pinned) doesn't false-fail.
|
|
314
|
+
digest_matched=0
|
|
315
|
+
if [[ -n "$EXPECTED_DIGEST" && -n "$rt_digest" && ( -z "$ACTIVE_RUNTIME_ID" || "$rt_id" == "$ACTIVE_RUNTIME_ID" ) ]]; then
|
|
316
|
+
if [[ "$rt_digest" != "$EXPECTED_DIGEST" ]]; then
|
|
317
|
+
is_clean=0
|
|
318
|
+
reasons+=("runtime image digest=${rt_digest} != expected digest=$EXPECTED_DIGEST")
|
|
319
|
+
else
|
|
320
|
+
digest_matched=1
|
|
321
|
+
fi
|
|
322
|
+
fi
|
|
323
|
+
# Ancestry check: a digest-pinned image recovers its source sha from the
|
|
324
|
+
# image's ECR tags. A matched expected digest is stronger provenance and
|
|
325
|
+
# skips ancestry.
|
|
326
|
+
if [[ -n "$rt_digest" && -z "$rt_image_sha" && "$digest_matched" -eq 0 ]]; then
|
|
327
|
+
rt_image_sha=$(source_sha_from_digest "$rt_image" "$rt_digest")
|
|
328
|
+
fi
|
|
329
|
+
if [[ "$digest_matched" -eq 0 && -n "$MIN_SOURCE_SHA" && ( -z "$ACTIVE_RUNTIME_ID" || "$rt_id" == "$ACTIVE_RUNTIME_ID" ) ]] && ! image_contains_source_sha "$rt_image_sha" "$MIN_SOURCE_SHA"; then
|
|
252
330
|
is_clean=0
|
|
253
331
|
reasons+=("runtime image sha=${rt_image_sha:-unknown} does not include required source sha=$MIN_SOURCE_SHA")
|
|
254
332
|
fi
|