@salesforce/afv-skills 1.55.0 → 1.56.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/package.json +1 -1
- package/skills/agentforce-generate/SKILL.md +8 -0
- package/skills/agentforce-generate/references/actions-reference.md +2 -2
- package/skills/agentforce-generate/references/voice-modality-reference.md +51 -15
- package/skills/agentforce-generate/references/voice-telephony-cli.md +193 -0
- package/skills/agentforce-observe/SKILL.md +13 -9
- package/skills/agentforce-observe/references/ahm-alerts.md +193 -14
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +24 -15
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +11 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +33 -1
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +32 -1
- package/skills/automation-sandbox-post-copy-configure/SKILL.md +75 -71
- package/skills/automation-sandbox-post-copy-configure/assets/scheduled_apex_template.apex +13 -0
- package/skills/automation-sandbox-post-copy-configure/examples/sample_scheduled_apex_config.json +13 -0
- package/skills/automation-sandbox-post-copy-configure/references/scheduled_apex_path.md +155 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/build-scheduled-apex.mjs +62 -0
- package/skills/automation-sandbox-post-copy-configure/scripts/soql-escape-job-name.mjs +28 -0
- package/skills/consumer-goods-accruals-datakit-deploy/SKILL.md +383 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/setup.js +324 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/00-check-prerequisites.js +63 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/01-download-static-resource.js +100 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/02-replace-org-id.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/03-deploy-metadata.js +46 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/04-deploy-engine.js +198 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/05-deploy-tpm-accruals.js +231 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/06-create-dataspace.js +82 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/07-deploy-accruals-reports.js +517 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/08-deploy-ui.js +416 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/09-completion.js +44 -0
- package/skills/consumer-goods-accruals-datakit-deploy/scripts/steps/utils.js +514 -0
- package/skills/consumer-goods-sync-management-configure/SKILL.md +137 -0
- package/skills/consumer-goods-sync-management-configure/examples/scenarios.md +294 -0
- package/skills/consumer-goods-sync-management-configure/references/act1-setup-sync.md +333 -0
- package/skills/consumer-goods-sync-management-configure/references/act2-assign-users.md +324 -0
- package/skills/consumer-goods-sync-management-configure/references/act3-plan-verify.md +113 -0
- package/skills/consumer-goods-sync-management-configure/references/readiness-and-enablement.md +164 -0
- package/skills/consumer-goods-sync-management-configure/references/sync-management-app-install-backbone.md +159 -0
- package/skills/consumer-goods-sync-management-configure/references/transports-and-namespace.md +220 -0
- package/skills/consumer-goods-sync-management-configure/references/verify-and-smoke.md +175 -0
- package/skills/dx-org-analyze/SKILL.md +0 -1
- package/skills/dx-org-analyze/scripts/collect_org_data.py +2 -2
- package/skills/dx-org-analyze/scripts/compute_diff.py +2 -4
- package/skills/dx-org-analyze/scripts/introspect_org.py +4 -40
- package/skills/dx-org-manage/SKILL.md +6 -50
- package/skills/dx-org-manage/examples/README.md +10 -16
- package/skills/dx-org-manage/references/scratch-org-create.md +1 -1
- package/skills/dx-org-manage/references/scratch-org-operations.md +1 -1
- package/skills/dx-org-shape-manage/SKILL.md +4 -3
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +1 -0
- package/skills/experience-accessibility-validate/SKILL.md +8 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-1-1-non-text-content.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-i-lists.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-ii-tables.md +20 -85
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iii-form-labels.md +4 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-iv-regions.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-1-v-groups.md +2 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-3-5-identify-input.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-1-4-3-contrast.md +4 -14
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-1-1-keyboard.md +9 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-4-link-purpose.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-4-6-headings-labels.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-1-pointer-gestures.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-2-pointer-cancellation.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-3-label-in-name.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-2-5-7-dragging-movement.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-1-on-focus.md +0 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-2-2-on-input.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-1-error-identification.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-2-labels-instructions.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-3-3-3-error-suggestion.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-i-name.md +4 -5
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-ii-role.md +1 -4
- package/skills/experience-accessibility-validate/references/reviewers/sc-4-1-2-iii-value.md +0 -4
- package/skills/experience-lwc-accessibility-jest-run/SKILL.md +104 -25
- package/skills/experience-lwc-accessibility-jest-run/assets/jest.config.js +4 -0
- package/skills/experience-lwc-accessibility-jest-run/assets/sa11y-jest-setup.js +5 -0
- package/skills/experience-ui-bundle-app-coordinate/SKILL.md +5 -5
- package/skills/experience-ui-bundle-features-generate/SKILL.md +65 -36
- package/skills/experience-ui-bundle-features-generate/references/angular/features.md +74 -0
- package/skills/experience-ui-bundle-features-generate/references/react/features.md +59 -0
- package/skills/experience-ui-bundle-features-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-frontend-generate/SKILL.md +2 -2
- package/skills/experience-ui-bundle-metadata-generate/SKILL.md +26 -14
- package/skills/experience-ui-bundle-metadata-generate/references/angular-metadata-generate.md +37 -0
- package/skills/experience-ui-bundle-metadata-generate/references/react-metadata-generate.md +32 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-metadata-generate/scripts/verify-bundle-location.sh +30 -8
- package/skills/field-service-data-capture-migrate/SKILL.md +391 -0
- package/skills/field-service-data-capture-migrate/assets/sample-legacy-flow.flow-meta.xml +163 -0
- package/skills/field-service-data-capture-migrate/references/architecture-notes.md +40 -0
- package/skills/field-service-data-capture-migrate/references/component-mapping.md +278 -0
- package/skills/field-service-data-capture-migrate/references/converter-builder-contract.md +183 -0
- package/skills/field-service-data-capture-migrate/references/dc-platform-rules.md +130 -0
- package/skills/field-service-data-capture-migrate/references/fsm-dc-capability-catalog.md +215 -0
- package/skills/field-service-data-capture-migrate/references/known-deploy-errors.md +36 -0
- package/skills/field-service-data-capture-migrate/references/migration-checklist.md +121 -0
- package/skills/field-service-data-capture-migrate/references/transformer-rules.md +132 -0
- package/skills/field-service-data-capture-migrate/scripts/analyze_flow.py +277 -0
- package/skills/field-service-data-capture-migrate/scripts/convert_to_dc_spec.py +1311 -0
- package/skills/field-service-data-capture-migrate/scripts/cud_analysis.py +402 -0
- package/skills/field-service-data-capture-migrate/scripts/deploy_flow.sh +230 -0
- package/skills/field-service-data-capture-migrate/scripts/discover_subflow_tree.py +230 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_input.py +186 -0
- package/skills/field-service-data-capture-migrate/scripts/flow_xml_utils.py +39 -0
- package/skills/field-service-data-capture-migrate/scripts/fsm_dc_catalog.py +413 -0
- package/skills/field-service-data-capture-migrate/scripts/generate_summary.py +199 -0
- package/skills/field-service-data-capture-migrate/scripts/json_to_flow_xml.py +146 -0
- package/skills/field-service-data-capture-migrate/scripts/manual_review_flags.py +42 -0
- package/skills/field-service-data-capture-migrate/scripts/retrieve_flow_rest.sh +155 -0
- package/skills/field-service-data-capture-migrate/scripts/subflow_names.py +49 -0
- package/skills/field-service-data-capture-migrate/scripts/transform_flow.py +1661 -0
- package/skills/field-service-data-capture-migrate/scripts/validate_flow.sh +147 -0
- package/skills/field-service-data-capture-migrate/scripts/vendor/build_flow.py +1258 -0
- package/skills/life-sciences-fieldsalesrep-coordinate/SKILL.md +1 -1
- package/skills/life-sciences-fieldsalesrep-coordinate/references/orchestration-flow.md +1 -1
- package/skills/life-sciences-kam-coordinate/references/stage-5-data-and-plan-templates-overview.md +14 -1
- package/skills/mobile-apps-create/SKILL.md +1 -1
- package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/SKILL.md +7 -7
- package/skills/platform-sandbox-configure/SKILL.md +108 -133
- package/skills/platform-sandbox-configure/references/api-response-shapes.md +39 -0
- package/skills/platform-sandbox-configure/references/definition-file-approach.md +73 -0
- package/skills/platform-widget-generate/SKILL.md +26 -18
- package/skills/platform-widget-generate/examples/formulas.json +191 -0
- package/skills/platform-widget-generate/references/widget-formulas.md +123 -0
- package/skills/platform-widget-generate/references/widget-meta-directives.md +4 -4
- package/skills/sales-call-scoring-configure/SKILL.md +318 -0
- package/skills/sales-call-scoring-configure/assets/best-practice-competencies.json +67 -0
- package/skills/sales-call-scoring-configure/assets/ootb-competencies.json +58 -0
- package/skills/sales-call-scoring-configure/references/admin-communication.md +79 -0
- package/skills/sales-call-scoring-configure/references/competency-crud.md +130 -0
- package/skills/sales-call-scoring-configure/scripts/create-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/create-custom-competency.sh +166 -0
- package/skills/sales-call-scoring-configure/scripts/edit-competency.sh +185 -0
- package/skills/sales-call-scoring-configure/scripts/edit-custom-competency.sh +190 -0
- package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +436 -0
- package/skills/sales-call-scoring-configure/scripts/get-competency.sh +72 -0
- package/skills/sales-call-scoring-configure/scripts/get-custom-competency.sh +78 -0
- package/skills/sales-call-scoring-configure/scripts/install-best-practice-competencies.sh +223 -0
- package/skills/sales-call-scoring-configure/scripts/install-ootb-competencies.sh +237 -0
- package/skills/sales-call-scoring-configure/scripts/list-competencies.sh +99 -0
- package/skills/sales-call-scoring-configure/scripts/shared/auth.sh +116 -0
- package/skills/sales-call-scoring-configure/scripts/shared/cap.sh +86 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soap.sh +267 -0
- package/skills/sales-call-scoring-configure/scripts/shared/soql.sh +25 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-competency.sh +112 -0
- package/skills/sales-call-scoring-configure/scripts/toggle-custom-competency.sh +125 -0
- package/skills/sales-call-scoring-configure/scripts/verify-ootb-competencies.sh +116 -0
- package/skills/service-concierge-portal-generate/references/portal-deploy-runbook.md +1 -0
- package/skills/service-de-channel-activate/SKILL.md +6 -5
- package/skills/service-de-channel-create/SKILL.md +3 -3
- package/skills/service-de-channel-routing-configure/SKILL.md +3 -3
- package/skills/service-de-channel-settings-configure/SKILL.md +195 -0
- package/skills/service-de-channel-settings-configure/references/channel-settings-facets.md +152 -0
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/gotchas.md +3 -3
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/language-keywords.md +4 -4
- package/skills/{service-de-channel-consent-configure → service-de-channel-settings-configure}/references/worked-examples.md +14 -14
- package/skills/service-de-headless-channel-configure/SKILL.md +6 -6
- package/skills/service-de-headless-channel-configure/references/worked-examples.md +1 -1
- package/skills/service-helpagent-coordinate/SKILL.md +3 -2
- package/skills/service-helpagent-coordinate/references/channel-voice.md +15 -4
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/SKILL.md +49 -31
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/examples/output-templates.md +71 -6
- package/skills/service-itsm-agentic-setup-agentforce-coordinate/scripts/verify-child-verdict.mjs +26 -4
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/SKILL.md +35 -10
- package/skills/service-itsm-agentic-setup-cmdb-access-assign/references/mcp-invocation.md +25 -5
- package/skills/service-itsm-agentic-setup-cmdb-bundle-deploy/SKILL.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-configure/SKILL.md +5 -5
- package/skills/service-itsm-agentic-setup-cmdb-configure/references/mcp-invocation.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/SKILL.md +2 -2
- package/skills/service-itsm-agentic-setup-cmdb-coordinate/examples/output-templates.md +1 -1
- package/skills/service-itsm-agentic-setup-cmdb-discovery-configure/SKILL.md +25 -15
- package/skills/service-itsm-agentic-setup-employee-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-employee-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-employee-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/SKILL.md +8 -5
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/report-format.md +4 -2
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/references/workflow-detail.md +1 -1
- package/skills/service-itsm-agentic-setup-fulfiller-agent-configure/scripts/render-report.mjs +29 -16
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +138 -68
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +31 -15
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +156 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +148 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +162 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +149 -0
- package/skills/service-itsm-agentic-setup-incident-sla-configure/SKILL.md +103 -89
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/attach-milestone.json +2 -2
- package/skills/service-itsm-agentic-setup-incident-sla-configure/assets/predefined-incident-policy.json +21 -39
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/milestone-patterns.md +3 -3
- package/skills/service-itsm-agentic-setup-incident-sla-configure/examples/output-templates.md +18 -1
- package/skills/service-itsm-agentic-setup-incident-sla-configure/references/mcp-invocation.md +63 -37
- package/skills/service-omni-attribute-routing-configure/SKILL.md +53 -0
- package/skills/service-omni-attribute-routing-configure/references/api-notes.md +7 -0
- package/skills/service-omni-attribute-routing-configure/scripts/configure-and-report.sh +224 -0
- package/skills/service-omni-attribute-routing-configure/scripts/tests/test_attribute_routing_contracts.py +137 -0
- package/skills/service-omni-channel-inventory-analyze/SKILL.md +96 -0
- package/skills/service-omni-channel-inventory-analyze/references/api-notes.md +52 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/analyze.sh +185 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/_bootstrap.py +84 -0
- package/skills/service-omni-channel-inventory-analyze/scripts/tests/test_inventory_analyze_contracts.py +141 -0
- package/skills/service-omni-channel-limits-analyze/SKILL.md +70 -0
- package/skills/service-omni-channel-limits-analyze/references/api-notes.md +22 -0
- package/skills/service-omni-channel-limits-analyze/scripts/analyze.sh +159 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/_bootstrap.py +87 -0
- package/skills/service-omni-channel-limits-analyze/scripts/tests/test_limits_analyze_contracts.py +99 -0
- package/skills/service-omni-channel-setup-coordinate/SKILL.md +12 -20
- package/skills/service-omni-channel-setup-coordinate/scripts/integration-driver.sh +55 -5
- package/skills/service-omni-channel-setup-coordinate/scripts/tests/test_omni_contracts.py +38 -0
- package/skills/service-omni-command-center-analyze/SKILL.md +2 -2
- package/skills/service-omni-command-center-analyze/scripts/analyze.sh +3 -3
- package/skills/dx-org-manage/examples/snapshots/error_output.json +0 -9
- package/skills/dx-org-manage/examples/snapshots/success_output.json +0 -15
- package/skills/dx-org-manage/references/cli_flags.md +0 -67
- package/skills/dx-org-manage/references/creating-snapshot.md +0 -103
- package/skills/dx-org-manage/references/snapshot_usage.md +0 -74
- package/skills/experience-accessibility-validate/scripts/contrast-ratio.py +0 -153
- package/skills/experience-lwc-accessibility-jest-run/references/running-sa11y-jest-tests.md +0 -205
- package/skills/experience-ui-bundle-features-generate/scripts/verify-react-bundle.sh +0 -14
- /package/skills/experience-ui-bundle-features-generate/references/{conflict-resolution-schema.json → common/conflict-resolution-schema.json} +0 -0
- /package/skills/{platform-agentsetup-categories-fetch → platform-agenticsetup-categories-get}/references/api-response-schema.md +0 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Ordinal CUD-placement analysis for FSM → DataCaptureFlow migration (WI-8).
|
|
4
|
+
|
|
5
|
+
DataCaptureFlow requires every record create/update/delete (CUD) to run at the
|
|
6
|
+
END of the flow, after all screens and data collection. FSM allows CUD anywhere.
|
|
7
|
+
A mid-flow CUD deploys but executes in the wrong order — silent data corruption.
|
|
8
|
+
|
|
9
|
+
This module computes, over the flow graph:
|
|
10
|
+
1. execution order (BFS from <start>);
|
|
11
|
+
2. each CUD's position relative to the last screen / last recordLookup;
|
|
12
|
+
3. a conservative safe/unsafe verdict for consolidating each mid-flow CUD.
|
|
13
|
+
|
|
14
|
+
Depends only on flow_xml_utils (no import cycle — safe to import from both
|
|
15
|
+
analyze_flow.py and convert_to_dc_spec.py).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import xml.etree.ElementTree as ET
|
|
19
|
+
from dataclasses import dataclass, field, asdict
|
|
20
|
+
from typing import Dict, List, Tuple
|
|
21
|
+
|
|
22
|
+
from flow_xml_utils import _findtext, _findall, _findall_deep # noqa: F401
|
|
23
|
+
|
|
24
|
+
CUD_TAGS = ("recordCreates", "recordUpdates", "recordDeletes")
|
|
25
|
+
|
|
26
|
+
# All named, connectable flow elements (document-order scan set).
|
|
27
|
+
_ELEMENT_TAGS = (
|
|
28
|
+
"screens", "decisions", "assignments", "recordCreates", "recordUpdates",
|
|
29
|
+
"recordDeletes", "recordLookups", "subflows", "loops",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_CONNECTOR_TAGS = (
|
|
33
|
+
"connector", "defaultConnector", "nextValueConnector", "noMoreValuesConnector",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _op_of_tag(tag: str) -> str:
|
|
38
|
+
return {"recordCreates": "create", "recordUpdates": "update",
|
|
39
|
+
"recordDeletes": "delete"}[tag]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _targets_of(el: ET.Element) -> List[str]:
|
|
43
|
+
"""All connector targetReferences carried by one element (incl. decision rules)."""
|
|
44
|
+
out: List[str] = []
|
|
45
|
+
for ct in _CONNECTOR_TAGS:
|
|
46
|
+
for conn in _findall(el, ct):
|
|
47
|
+
t = _findtext(conn, "targetReference")
|
|
48
|
+
if t:
|
|
49
|
+
out.append(t)
|
|
50
|
+
for rule in _findall(el, "rules"):
|
|
51
|
+
for conn in _findall(rule, "connector"):
|
|
52
|
+
t = _findtext(conn, "targetReference")
|
|
53
|
+
if t:
|
|
54
|
+
out.append(t)
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _element_index(root: ET.Element) -> Dict[str, Tuple[str, ET.Element]]:
|
|
59
|
+
"""Map element api-name -> (localTag, element) for named flow elements."""
|
|
60
|
+
idx: Dict[str, Tuple[str, ET.Element]] = {}
|
|
61
|
+
for tag in _ELEMENT_TAGS:
|
|
62
|
+
for el in _findall_deep(root, tag):
|
|
63
|
+
name = _findtext(el, "name")
|
|
64
|
+
if name and name not in idx:
|
|
65
|
+
idx[name] = (tag, el)
|
|
66
|
+
return idx
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _start_target(root: ET.Element) -> str:
|
|
70
|
+
for s in _findall(root, "start"):
|
|
71
|
+
for conn in _findall(s, "connector"):
|
|
72
|
+
t = _findtext(conn, "targetReference")
|
|
73
|
+
if t:
|
|
74
|
+
return t
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def execution_order(root: ET.Element) -> List[str]:
|
|
79
|
+
"""Element api-names in BFS execution order from <start>. Cycle-guarded."""
|
|
80
|
+
idx = _element_index(root)
|
|
81
|
+
start = _start_target(root)
|
|
82
|
+
visited: set = set()
|
|
83
|
+
order: List[str] = []
|
|
84
|
+
queue: List[str] = [start] if start else []
|
|
85
|
+
while queue:
|
|
86
|
+
n = queue.pop(0)
|
|
87
|
+
if not n or n in visited or n not in idx:
|
|
88
|
+
continue
|
|
89
|
+
visited.add(n)
|
|
90
|
+
order.append(n)
|
|
91
|
+
queue.extend(_targets_of(idx[n][1]))
|
|
92
|
+
return order
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class CudNode:
|
|
97
|
+
name: str
|
|
98
|
+
op: str
|
|
99
|
+
position: int
|
|
100
|
+
after_last_screen: bool
|
|
101
|
+
container: str = "top"
|
|
102
|
+
verdict: str = "compliant"
|
|
103
|
+
blockers: List[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class CudPlacementReport:
|
|
108
|
+
has_midflow: bool
|
|
109
|
+
cud_nodes: List[CudNode]
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict:
|
|
112
|
+
return asdict(self)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _cud_names(root: ET.Element) -> Dict[str, str]:
|
|
116
|
+
"""api-name -> op for every CUD element in the flow."""
|
|
117
|
+
out: Dict[str, str] = {}
|
|
118
|
+
for tag in CUD_TAGS:
|
|
119
|
+
for el in _findall_deep(root, tag):
|
|
120
|
+
name = _findtext(el, "name")
|
|
121
|
+
if name:
|
|
122
|
+
out[name] = _op_of_tag(tag)
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _loop_member_names(root: ET.Element) -> Dict[str, str]:
|
|
127
|
+
"""api-name -> loop name for elements inside a loop body.
|
|
128
|
+
|
|
129
|
+
Body = everything reachable from the loop's nextValueConnector, stopping at
|
|
130
|
+
the loop node itself and at the noMoreValuesConnector target (loop exit).
|
|
131
|
+
"""
|
|
132
|
+
idx = _element_index(root)
|
|
133
|
+
members: Dict[str, str] = {}
|
|
134
|
+
for lp in _findall_deep(root, "loops"):
|
|
135
|
+
loop_name = _findtext(lp, "name")
|
|
136
|
+
if not loop_name:
|
|
137
|
+
continue
|
|
138
|
+
exits = {_findtext(c, "targetReference")
|
|
139
|
+
for c in _findall(lp, "noMoreValuesConnector")}
|
|
140
|
+
starts = [_findtext(c, "targetReference")
|
|
141
|
+
for c in _findall(lp, "nextValueConnector")]
|
|
142
|
+
seen: set = set()
|
|
143
|
+
queue = [s for s in starts if s]
|
|
144
|
+
while queue:
|
|
145
|
+
n = queue.pop(0)
|
|
146
|
+
if not n or n in seen or n == loop_name or n in exits or n not in idx:
|
|
147
|
+
continue
|
|
148
|
+
seen.add(n)
|
|
149
|
+
members.setdefault(n, loop_name)
|
|
150
|
+
queue.extend(_targets_of(idx[n][1]))
|
|
151
|
+
return members
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _decision_branch_only(root: ET.Element) -> Dict[str, str]:
|
|
155
|
+
"""api-name -> decision name for elements reachable ONLY via a decision branch.
|
|
156
|
+
|
|
157
|
+
An element is 'branch-only' if every inbound connector that reaches it comes
|
|
158
|
+
from a decision (either <rules><connector> OR <defaultConnector>), not from
|
|
159
|
+
<start> or a plain sequential connector from a non-decision element.
|
|
160
|
+
"""
|
|
161
|
+
idx = _element_index(root)
|
|
162
|
+
branch_sources: Dict[str, str] = {} # target -> decision name (via rule OR default)
|
|
163
|
+
other_inbound: set = set() # targets reached by non-decision connectors
|
|
164
|
+
|
|
165
|
+
start = _start_target(root)
|
|
166
|
+
if start:
|
|
167
|
+
other_inbound.add(start)
|
|
168
|
+
|
|
169
|
+
for name, (tag, el) in idx.items():
|
|
170
|
+
if tag == "decisions":
|
|
171
|
+
# Both rule connectors AND default connectors are conditional branches
|
|
172
|
+
for rule in _findall(el, "rules"):
|
|
173
|
+
for conn in _findall(rule, "connector"):
|
|
174
|
+
t = _findtext(conn, "targetReference")
|
|
175
|
+
if t:
|
|
176
|
+
branch_sources.setdefault(t, name)
|
|
177
|
+
for conn in _findall(el, "defaultConnector"):
|
|
178
|
+
t = _findtext(conn, "targetReference")
|
|
179
|
+
if t:
|
|
180
|
+
branch_sources.setdefault(t, name)
|
|
181
|
+
else:
|
|
182
|
+
# Non-decision connectors make targets unconditionally reachable
|
|
183
|
+
for ct in _CONNECTOR_TAGS:
|
|
184
|
+
for conn in _findall(el, ct):
|
|
185
|
+
t = _findtext(conn, "targetReference")
|
|
186
|
+
if t:
|
|
187
|
+
other_inbound.add(t)
|
|
188
|
+
|
|
189
|
+
return {t: dec for t, dec in branch_sources.items() if t not in other_inbound}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _cud_output_refs(cud_el: ET.Element, op: str) -> List[str]:
|
|
193
|
+
"""Reference tokens a downstream element would use to consume this CUD's result."""
|
|
194
|
+
tokens: List[str] = []
|
|
195
|
+
name = _findtext(cud_el, "name")
|
|
196
|
+
if name:
|
|
197
|
+
tokens.append(name)
|
|
198
|
+
assign_id = _findtext(cud_el, "assignRecordIdToReference")
|
|
199
|
+
if assign_id:
|
|
200
|
+
tokens.append(assign_id)
|
|
201
|
+
out_ref = _findtext(cud_el, "outputReference")
|
|
202
|
+
if out_ref:
|
|
203
|
+
tokens.append(out_ref)
|
|
204
|
+
return tokens
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _downstream_reference(root: ET.Element, order: List[str], cud_name: str,
|
|
208
|
+
tokens: List[str]) -> str:
|
|
209
|
+
"""Name of the first element after cud_name in `order` that references a token, else ''."""
|
|
210
|
+
idx = _element_index(root)
|
|
211
|
+
try:
|
|
212
|
+
after = order[order.index(cud_name) + 1:]
|
|
213
|
+
except ValueError:
|
|
214
|
+
after = []
|
|
215
|
+
for name in after:
|
|
216
|
+
if name not in idx:
|
|
217
|
+
continue
|
|
218
|
+
blob = ET.tostring(idx[name][1], encoding="unicode")
|
|
219
|
+
for tok in tokens:
|
|
220
|
+
# Match the token as a whole reference (avoid substring false hits).
|
|
221
|
+
# Includes: element text (>tok<), dotted references (tok.), and
|
|
222
|
+
# merge-field syntax ({!tok} or {!tok.Field}).
|
|
223
|
+
if (f">{tok}<" in blob or f"{tok}." in blob or
|
|
224
|
+
f"{{!{tok}}}" in blob or f"{{!{tok}." in blob):
|
|
225
|
+
return name
|
|
226
|
+
return ""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _check_input_references(cud_el: ET.Element, root: ET.Element) -> str:
|
|
230
|
+
"""Check if create's inputAssignments reference variables that survive migration.
|
|
231
|
+
|
|
232
|
+
Returns blocker message if any input is unresolvable, else empty string.
|
|
233
|
+
A reference is safe if its root resolves to:
|
|
234
|
+
- a screen field name (any <screens><fields><name>)
|
|
235
|
+
- a screen element name (any <screens><name>)
|
|
236
|
+
- a DC standard input variable (recordId, parentRecordId, parentObjectType)
|
|
237
|
+
"""
|
|
238
|
+
idx = _element_index(root)
|
|
239
|
+
|
|
240
|
+
# Collect all screen field names and screen names
|
|
241
|
+
safe_refs: set = set()
|
|
242
|
+
for tag, el in idx.values():
|
|
243
|
+
if tag == "screens":
|
|
244
|
+
screen_name = _findtext(el, "name")
|
|
245
|
+
if screen_name:
|
|
246
|
+
safe_refs.add(screen_name)
|
|
247
|
+
for fld in _findall(el, "fields"):
|
|
248
|
+
fld_name = _findtext(fld, "name")
|
|
249
|
+
if fld_name:
|
|
250
|
+
safe_refs.add(fld_name)
|
|
251
|
+
|
|
252
|
+
# DC standard input variables (confirmed from build_flow.py line 1176)
|
|
253
|
+
safe_refs.update(["recordId", "parentRecordId", "parentObjectType"])
|
|
254
|
+
|
|
255
|
+
# Check each inputAssignments elementReference
|
|
256
|
+
for inp_asg in _findall(cud_el, "inputAssignments"):
|
|
257
|
+
for val in _findall(inp_asg, "value"):
|
|
258
|
+
ref = _findtext(val, "elementReference")
|
|
259
|
+
if ref:
|
|
260
|
+
# Extract root (part before first dot)
|
|
261
|
+
root_ref = ref.split(".")[0].split("!")[0].strip("{}")
|
|
262
|
+
if root_ref and root_ref not in safe_refs:
|
|
263
|
+
return f"input references {root_ref} which does not survive migration"
|
|
264
|
+
|
|
265
|
+
return ""
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _has_screen_or_lookup_downstream(root: ET.Element, cud_name: str,
|
|
269
|
+
idx: Dict[str, Tuple[str, ET.Element]]) -> bool:
|
|
270
|
+
"""Check if any screen or recordLookup is reachable forward from cud_name.
|
|
271
|
+
|
|
272
|
+
Uses connector-based reachability (BFS), mirroring detect_ceiling_violations.
|
|
273
|
+
"""
|
|
274
|
+
if cud_name not in idx:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
visited: set = set()
|
|
278
|
+
frontier = _targets_of(idx[cud_name][1])
|
|
279
|
+
|
|
280
|
+
while frontier:
|
|
281
|
+
nxt = frontier.pop()
|
|
282
|
+
if nxt in visited or nxt not in idx:
|
|
283
|
+
continue
|
|
284
|
+
visited.add(nxt)
|
|
285
|
+
|
|
286
|
+
tag = idx[nxt][0]
|
|
287
|
+
if tag in ("screens", "recordLookups"):
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
frontier.extend(_targets_of(idx[nxt][1]))
|
|
291
|
+
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _completion_bypasses(root: ET.Element, idx: Dict[str, Tuple[str, ET.Element]],
|
|
296
|
+
cud_name: str) -> bool:
|
|
297
|
+
"""True if some path start->completion reaches a terminal WITHOUT the CUD.
|
|
298
|
+
|
|
299
|
+
A create is safe to auto-consolidate only when unconditional (every
|
|
300
|
+
completion path runs it). If a completion terminal is reachable while
|
|
301
|
+
skipping the CUD, the create is conditional -> unsafe to consolidate.
|
|
302
|
+
"""
|
|
303
|
+
start = _start_target(root)
|
|
304
|
+
if not start or cud_name not in idx:
|
|
305
|
+
return False # unreachable/degenerate handled elsewhere; don't add noise
|
|
306
|
+
seen: set = set()
|
|
307
|
+
queue = [start]
|
|
308
|
+
while queue:
|
|
309
|
+
n = queue.pop(0)
|
|
310
|
+
if not n or n == cud_name or n in seen or n not in idx:
|
|
311
|
+
continue
|
|
312
|
+
seen.add(n)
|
|
313
|
+
outs = [t for t in _targets_of(idx[n][1]) if t in idx]
|
|
314
|
+
if not outs:
|
|
315
|
+
return True # reached a completion terminal without the CUD
|
|
316
|
+
queue.extend(outs)
|
|
317
|
+
return False
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def analyze_cud_placement(root: ET.Element) -> CudPlacementReport:
|
|
321
|
+
order = execution_order(root)
|
|
322
|
+
pos = {name: i for i, name in enumerate(order)}
|
|
323
|
+
idx = _element_index(root)
|
|
324
|
+
cuds = _cud_names(root)
|
|
325
|
+
|
|
326
|
+
loop_members = _loop_member_names(root)
|
|
327
|
+
branch_only = _decision_branch_only(root)
|
|
328
|
+
|
|
329
|
+
nodes: List[CudNode] = []
|
|
330
|
+
has_midflow = False
|
|
331
|
+
for name, op in cuds.items():
|
|
332
|
+
p = pos.get(name, -1)
|
|
333
|
+
|
|
334
|
+
# Determine after_last_screen via forward reachability (C2 fix).
|
|
335
|
+
# A CUD is compliant (after last screen) ONLY IF no screen/recordLookup
|
|
336
|
+
# is reachable downstream from it.
|
|
337
|
+
after = (p != -1 and not _has_screen_or_lookup_downstream(root, name, idx))
|
|
338
|
+
node = CudNode(name=name, op=op, position=p, after_last_screen=after)
|
|
339
|
+
|
|
340
|
+
# Check for container issues first (loop or decision-branch) even if after barrier
|
|
341
|
+
if name in loop_members:
|
|
342
|
+
node.container = f"loop:{loop_members[name]}"
|
|
343
|
+
node.verdict = "unsafe_manual"
|
|
344
|
+
node.blockers.append(f"inside loop {loop_members[name]}")
|
|
345
|
+
has_midflow = True
|
|
346
|
+
elif name in branch_only and not after:
|
|
347
|
+
# Branch-only CUD that's mid-flow (has screens downstream)
|
|
348
|
+
# Post-screen decision branches are allowed in Data Capture
|
|
349
|
+
node.container = f"decisionBranch:{branch_only[name]}"
|
|
350
|
+
node.verdict = "unsafe_manual"
|
|
351
|
+
node.blockers.append(
|
|
352
|
+
f"mid-flow decision branch {branch_only[name]} (screens downstream)")
|
|
353
|
+
has_midflow = True
|
|
354
|
+
elif p == -1:
|
|
355
|
+
# Unreachable CUD: cannot determine ordering, requires manual review
|
|
356
|
+
node.verdict = "unsafe_manual"
|
|
357
|
+
node.blockers.append(f"unreachable from start (no incoming connector)")
|
|
358
|
+
has_midflow = True
|
|
359
|
+
elif after:
|
|
360
|
+
# Truly compliant - no screen/lookup downstream and not in a problematic container
|
|
361
|
+
# BUT: for creates, still check input references (I1 fix)
|
|
362
|
+
cud_el = idx[name][1]
|
|
363
|
+
input_blocker = ""
|
|
364
|
+
if op == "create":
|
|
365
|
+
input_blocker = _check_input_references(cud_el, root)
|
|
366
|
+
|
|
367
|
+
if input_blocker:
|
|
368
|
+
node.verdict = "unsafe_manual"
|
|
369
|
+
node.blockers.append(input_blocker)
|
|
370
|
+
has_midflow = True
|
|
371
|
+
else:
|
|
372
|
+
node.verdict = "compliant"
|
|
373
|
+
else:
|
|
374
|
+
# Mid-flow CUD, check for downstream references and input validity
|
|
375
|
+
has_midflow = True
|
|
376
|
+
cud_el = idx[name][1]
|
|
377
|
+
tokens = _cud_output_refs(cud_el, op)
|
|
378
|
+
ref = _downstream_reference(root, order, name, tokens)
|
|
379
|
+
|
|
380
|
+
# I1 fix: for creates, check input assignments BEFORE classifying as safe
|
|
381
|
+
input_blocker = ""
|
|
382
|
+
if op == "create":
|
|
383
|
+
input_blocker = _check_input_references(cud_el, root)
|
|
384
|
+
|
|
385
|
+
if ref:
|
|
386
|
+
node.verdict = "unsafe_manual"
|
|
387
|
+
node.blockers.append(f"output referenced by {ref}")
|
|
388
|
+
elif input_blocker:
|
|
389
|
+
node.verdict = "unsafe_manual"
|
|
390
|
+
node.blockers.append(input_blocker)
|
|
391
|
+
elif op == "create" and _completion_bypasses(root, idx, name):
|
|
392
|
+
# WI-8 residual fix: conditional create (bypassable via some completion path)
|
|
393
|
+
node.verdict = "unsafe_manual"
|
|
394
|
+
node.blockers.append(
|
|
395
|
+
"conditional: some completion paths bypass this create; "
|
|
396
|
+
"consolidating would make the write unconditional")
|
|
397
|
+
else:
|
|
398
|
+
node.verdict = "safe_consolidate"
|
|
399
|
+
nodes.append(node)
|
|
400
|
+
|
|
401
|
+
nodes.sort(key=lambda n: (n.position if n.position >= 0 else 1_000_000, n.name))
|
|
402
|
+
return CudPlacementReport(has_midflow=has_midflow, cud_nodes=nodes)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Deploy migrated DataCaptureFlow output to a customer org, capturing a
|
|
3
|
+
# pre-deploy rollback snapshot first. Demo-ready implementation of FR-013
|
|
4
|
+
# (deploy kickoff) and FR-014 (rollback snapshot).
|
|
5
|
+
#
|
|
6
|
+
# Order of operations:
|
|
7
|
+
# 1. FR-014 snapshot — query each target flow's prior state in the org and
|
|
8
|
+
# write a rollback manifest BEFORE anything is written. Reversibility first.
|
|
9
|
+
# 2. Confirmation — deploy NEVER runs automatically; the SE must confirm
|
|
10
|
+
# (interactive prompt, or --yes for demo automation). Hard product constraint.
|
|
11
|
+
# 3. FR-013 deploy — `sf project deploy start` (real deploy). Every deploy
|
|
12
|
+
# copy is force-set to <status>Draft</status> regardless of the source
|
|
13
|
+
# file's on-disk status, in the leaves-first order the caller passes
|
|
14
|
+
# (Step 3b dependency order). Activation is never done by this script —
|
|
15
|
+
# it is a manual admin action in the org UI (SKILL.md Step 8), after
|
|
16
|
+
# validation and device testing.
|
|
17
|
+
#
|
|
18
|
+
# Offline-safe: with no org alias, nothing is queried or written and the skill
|
|
19
|
+
# still succeeds (validation/deploy deferred until an org is available).
|
|
20
|
+
#
|
|
21
|
+
# Usage: deploy_flow.sh [--yes] <org-alias> <flow-file> [<flow-file> ...]
|
|
22
|
+
# --yes : skip the interactive confirmation prompt (demo automation)
|
|
23
|
+
# arg 1 : org alias ("" = offline, skip deploy)
|
|
24
|
+
# args 2..N : paths to .flow-meta.xml files, in deploy order (leaves first)
|
|
25
|
+
#
|
|
26
|
+
# Exit codes:
|
|
27
|
+
# 0 deploy succeeded
|
|
28
|
+
# 1 deploy failed (or a flow file was not found)
|
|
29
|
+
# 2 usage error (no flow files given)
|
|
30
|
+
# 3 deploy skipped (no target org) — nothing was written
|
|
31
|
+
# 4 aborted by SE (declined confirmation) — nothing was written
|
|
32
|
+
|
|
33
|
+
set -uo pipefail
|
|
34
|
+
|
|
35
|
+
SKIP_MSG="No target org configured. Deploy skipped (nothing written)."
|
|
36
|
+
SNAPSHOT_ROOT="${FSM_ROLLBACK_ROOT:-/tmp/fsm-migration/rollback}"
|
|
37
|
+
|
|
38
|
+
ASSUME_YES=0
|
|
39
|
+
while [[ "${1:-}" == "--yes" ]]; do
|
|
40
|
+
ASSUME_YES=1
|
|
41
|
+
shift
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
ALIAS="${1:-}"
|
|
45
|
+
shift || true
|
|
46
|
+
FLOWS=("$@")
|
|
47
|
+
|
|
48
|
+
if [[ ${#FLOWS[@]} -eq 0 ]]; then
|
|
49
|
+
echo "Usage: deploy_flow.sh [--yes] <org-alias> <flow-file> [<flow-file> ...]" >&2
|
|
50
|
+
exit 2
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [[ -z "$ALIAS" ]]; then
|
|
54
|
+
echo "$SKIP_MSG"
|
|
55
|
+
echo "DEPLOY SUMMARY: skipped (no target org)"
|
|
56
|
+
exit 3
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# Verify every file exists before touching the org.
|
|
60
|
+
for FLOW in "${FLOWS[@]}"; do
|
|
61
|
+
if [[ ! -f "$FLOW" ]]; then
|
|
62
|
+
echo "ERROR: flow file not found: $FLOW" >&2
|
|
63
|
+
echo "DEPLOY SUMMARY: 0 deployed, aborted (missing file)"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
done
|
|
67
|
+
|
|
68
|
+
# API name = filename minus the .flow-meta.xml suffix.
|
|
69
|
+
API_NAMES=()
|
|
70
|
+
for FLOW in "${FLOWS[@]}"; do
|
|
71
|
+
API_NAMES+=("$(basename "$FLOW" .flow-meta.xml)")
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
# ── Step 1 — FR-014 rollback snapshot (READ-only, before any write) ──────────
|
|
75
|
+
# Query each target flow's current state in the org so a later restore knows the
|
|
76
|
+
# pre-deploy version/status. A flow with no prior state is a brand-new deploy
|
|
77
|
+
# (nothing to roll back to except removal — out of scope per work item).
|
|
78
|
+
# The snapshot is safety-critical: any failure to capture or render it aborts
|
|
79
|
+
# the deploy (we never write without a snapshot in hand).
|
|
80
|
+
STAMP=$(date +%Y%m%d-%H%M%S)-$$
|
|
81
|
+
SNAPSHOT_DIR="$SNAPSHOT_ROOT/$STAMP"
|
|
82
|
+
mkdir -p "$SNAPSHOT_DIR"
|
|
83
|
+
MANIFEST="$SNAPSHOT_DIR/snapshot.json"
|
|
84
|
+
|
|
85
|
+
abort_snapshot() {
|
|
86
|
+
echo "ERROR: $1 — refusing to deploy without a rollback snapshot." >&2
|
|
87
|
+
echo "DEPLOY SUMMARY: 0 deployed, aborted (snapshot failed)"
|
|
88
|
+
exit 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Build a SOQL IN-list of the target API names.
|
|
92
|
+
IN_LIST=""
|
|
93
|
+
for NAME in "${API_NAMES[@]}"; do
|
|
94
|
+
[[ -n "$IN_LIST" ]] && IN_LIST+=","
|
|
95
|
+
IN_LIST+="'$NAME'"
|
|
96
|
+
done
|
|
97
|
+
|
|
98
|
+
# NOTE: no `2>&1` — the CLI prints warnings (update notices, apiVersion) to
|
|
99
|
+
# stderr even on success, and merging them would corrupt the JSON we parse.
|
|
100
|
+
SNAP_QUERY="SELECT ApiName, Label, IsActive, ActiveVersionId, LatestVersionId FROM FlowDefinitionView WHERE ApiName IN ($IN_LIST)"
|
|
101
|
+
SNAP_OUT=$(sf data query --target-org "$ALIAS" --query "$SNAP_QUERY" --json)
|
|
102
|
+
SNAP_RC=$?
|
|
103
|
+
[[ $SNAP_RC -ne 0 ]] && abort_snapshot "rollback snapshot query failed (sf exit $SNAP_RC)"
|
|
104
|
+
|
|
105
|
+
# Render the manifest: one entry per REQUESTED flow, marking whether it already
|
|
106
|
+
# existed in the org (existedBeforeDeploy) and its prior state if so. The
|
|
107
|
+
# renderer prints the existing-flow count on its last stdout line so the shell
|
|
108
|
+
# doesn't have to re-parse the manifest. Any parse/render error exits nonzero.
|
|
109
|
+
EXISTING=$(printf '%s' "$SNAP_OUT" | ORG="$ALIAS" STAMP="$STAMP" NAMES="${API_NAMES[*]}" MANIFEST="$MANIFEST" python3 -c '
|
|
110
|
+
import sys, json, os
|
|
111
|
+
try:
|
|
112
|
+
data = json.load(sys.stdin)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
sys.stderr.write("could not parse snapshot query JSON: %s\n" % e)
|
|
115
|
+
sys.exit(1)
|
|
116
|
+
records = {r["ApiName"]: r for r in data.get("result", {}).get("records", [])}
|
|
117
|
+
requested = os.environ["NAMES"].split()
|
|
118
|
+
flows, existing = [], 0
|
|
119
|
+
for name in requested:
|
|
120
|
+
r = records.get(name)
|
|
121
|
+
if r:
|
|
122
|
+
existing += 1
|
|
123
|
+
flows.append({
|
|
124
|
+
"apiName": name,
|
|
125
|
+
"existedBeforeDeploy": True,
|
|
126
|
+
"priorLabel": r.get("Label"),
|
|
127
|
+
"priorIsActive": r.get("IsActive"),
|
|
128
|
+
"priorActiveVersionId": r.get("ActiveVersionId"),
|
|
129
|
+
"priorLatestVersionId": r.get("LatestVersionId"),
|
|
130
|
+
})
|
|
131
|
+
else:
|
|
132
|
+
flows.append({"apiName": name, "existedBeforeDeploy": False})
|
|
133
|
+
manifest = {"capturedAt": os.environ["STAMP"], "org": os.environ["ORG"], "flows": flows}
|
|
134
|
+
with open(os.environ["MANIFEST"], "w") as f:
|
|
135
|
+
json.dump(manifest, f, indent=2)
|
|
136
|
+
print(existing)
|
|
137
|
+
')
|
|
138
|
+
[[ $? -ne 0 ]] && abort_snapshot "rollback snapshot could not be rendered"
|
|
139
|
+
|
|
140
|
+
NEW_COUNT=$(( ${#API_NAMES[@]} - EXISTING ))
|
|
141
|
+
echo "Rollback snapshot: $MANIFEST"
|
|
142
|
+
echo " ${#API_NAMES[@]} flow(s) targeted; $EXISTING already exist in $ALIAS (restorable), $NEW_COUNT new."
|
|
143
|
+
|
|
144
|
+
# ── Step 2 — Confirmation (no auto-deploy) ───────────────────────────────────
|
|
145
|
+
# Deploy is always Draft — see Step 3 below for the force-Draft rewrite.
|
|
146
|
+
DEPLOY_STATUS="Draft"
|
|
147
|
+
if [[ $ASSUME_YES -ne 1 ]]; then
|
|
148
|
+
echo
|
|
149
|
+
echo "About to deploy ${#FLOWS[@]} flow(s) to org '$ALIAS' as $DEPLOY_STATUS:"
|
|
150
|
+
for NAME in "${API_NAMES[@]}"; do echo " - $NAME"; done
|
|
151
|
+
printf "Proceed? [y/N] "
|
|
152
|
+
read -r REPLY < /dev/tty || REPLY=""
|
|
153
|
+
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
|
|
154
|
+
echo "Aborted by user. Nothing deployed. Snapshot retained at $MANIFEST"
|
|
155
|
+
echo "DEPLOY SUMMARY: 0 deployed, aborted (user declined)"
|
|
156
|
+
exit 4
|
|
157
|
+
fi
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# ── Step 3 — FR-013 deploy (real, leaves-first) ──────────────────────────────
|
|
161
|
+
# Deploy-boundary guarantee: every copy is force-set to <status>Draft</status>
|
|
162
|
+
# here, unconditionally — regardless of what transform_flow.py wrote, and
|
|
163
|
+
# regardless of what a hand-edited or non-transformer source file contains.
|
|
164
|
+
# This script never deploys an Active flow. The only way a migrated flow goes
|
|
165
|
+
# Active is a human clicking Activate in the org UI after validation and
|
|
166
|
+
# device testing (SKILL.md Step 8) — never a flag on this script.
|
|
167
|
+
WORKDIR=$(mktemp -d -t dcflow-deploy.XXXXXX)
|
|
168
|
+
trap 'rm -rf "$WORKDIR"' EXIT
|
|
169
|
+
mkdir -p "$WORKDIR/force-app/main/default/flows"
|
|
170
|
+
for FLOW in "${FLOWS[@]}"; do
|
|
171
|
+
DEST="$WORKDIR/force-app/main/default/flows/$(basename "$FLOW")"
|
|
172
|
+
cp "$FLOW" "$DEST"
|
|
173
|
+
# Force <status>Draft</status> in the deploy copy. If the file has no
|
|
174
|
+
# <status> element, inject one before </Flow>. Portable across BSD/GNU sed
|
|
175
|
+
# via a Python one-liner. Source file on disk is never modified.
|
|
176
|
+
SRC="$DEST" python3 - <<'PY'
|
|
177
|
+
import os, re
|
|
178
|
+
p = os.environ["SRC"]
|
|
179
|
+
s = open(p).read()
|
|
180
|
+
if re.search(r"<status>.*?</status>", s, flags=re.S):
|
|
181
|
+
s = re.sub(r"<status>.*?</status>", "<status>Draft</status>", s, count=1, flags=re.S)
|
|
182
|
+
else:
|
|
183
|
+
s = s.replace("</Flow>", " <status>Draft</status>\n</Flow>", 1)
|
|
184
|
+
open(p, "w").write(s)
|
|
185
|
+
PY
|
|
186
|
+
done
|
|
187
|
+
cat > "$WORKDIR/sfdx-project.json" <<'JSON'
|
|
188
|
+
{
|
|
189
|
+
"packageDirectories": [{"path": "force-app", "default": true}],
|
|
190
|
+
"namespace": "",
|
|
191
|
+
"sfdcLoginUrl": "https://login.salesforce.com",
|
|
192
|
+
"sourceApiVersion": "67.0"
|
|
193
|
+
}
|
|
194
|
+
JSON
|
|
195
|
+
|
|
196
|
+
# No `2>&1`: keep CLI stderr warnings out of the JSON we parse. Deploy errors
|
|
197
|
+
# come back through --json as componentFailures.
|
|
198
|
+
OUTPUT=$(cd "$WORKDIR" && sf project deploy start \
|
|
199
|
+
--source-dir "force-app/main/default/flows" \
|
|
200
|
+
--target-org "$ALIAS" \
|
|
201
|
+
--wait 60 \
|
|
202
|
+
--json)
|
|
203
|
+
RC=$?
|
|
204
|
+
|
|
205
|
+
# Renderer prints the human status lines to stderr and the deployed/error counts
|
|
206
|
+
# ("<deployed> <errors>") on its single stdout line for the summary.
|
|
207
|
+
COUNTS=$(printf '%s' "$OUTPUT" | python3 -c '
|
|
208
|
+
import sys, json
|
|
209
|
+
try:
|
|
210
|
+
d = json.load(sys.stdin)
|
|
211
|
+
except Exception:
|
|
212
|
+
sys.stderr.write(" (could not parse sf output)\n"); print("? ?"); sys.exit(0)
|
|
213
|
+
r = d.get("result", {})
|
|
214
|
+
sys.stderr.write("Status: %s Deployed: %s Errors: %s\n" % (
|
|
215
|
+
r.get("status"), r.get("numberComponentsDeployed", 0), r.get("numberComponentErrors", 0)))
|
|
216
|
+
for e in r.get("details", {}).get("componentFailures", []):
|
|
217
|
+
sys.stderr.write(" ERROR: %s — %s\n" % (e.get("fullName"), e.get("problem")))
|
|
218
|
+
print("%s %s" % (r.get("numberComponentsDeployed", 0), r.get("numberComponentErrors", 0)))
|
|
219
|
+
')
|
|
220
|
+
DEPLOYED="${COUNTS% *}"
|
|
221
|
+
ERRS="${COUNTS#* }"
|
|
222
|
+
|
|
223
|
+
if [[ $RC -eq 0 ]]; then
|
|
224
|
+
echo "DEPLOY SUMMARY: $DEPLOYED deployed as $DEPLOY_STATUS, $ERRS errors (org: $ALIAS)"
|
|
225
|
+
echo "To roll back, see snapshot: $MANIFEST"
|
|
226
|
+
exit 0
|
|
227
|
+
else
|
|
228
|
+
echo "DEPLOY SUMMARY: deploy failed (org: $ALIAS) — see errors above. Snapshot: $MANIFEST"
|
|
229
|
+
exit 1
|
|
230
|
+
fi
|