dsh-d365-kit 0.34.1 → 0.34.4
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/CHANGELOG.md +319 -308
- package/CHANGELOG.zh-CN.md +318 -307
- package/INSTALL.md +94 -94
- package/INSTALL.zh-CN.md +93 -93
- package/LICENSE +21 -21
- package/README.md +336 -327
- package/README.zh-CN.md +336 -327
- package/client.js +1818 -1818
- package/cordis.patch.yml +44 -44
- package/docs/en/architecture.md +172 -172
- package/docs/en/checks.md +184 -184
- package/docs/en/installation.md +151 -151
- package/docs/en/permissions.md +161 -161
- package/docs/en/scenarios.md +202 -202
- package/docs/en/tools.md +665 -665
- package/docs/en/troubleshooting.md +131 -131
- package/docs/zh/architecture.md +172 -172
- package/docs/zh/checks.md +184 -184
- package/docs/zh/installation.md +151 -151
- package/docs/zh/permissions.md +161 -161
- package/docs/zh/scenarios.md +202 -202
- package/docs/zh/tools.md +665 -665
- package/docs/zh/troubleshooting.md +131 -131
- package/examples/prompts.en.md +206 -206
- package/examples/prompts.zh-CN.md +206 -206
- package/examples/waivers.example.json +20 -20
- package/icon.svg +6 -6
- package/index.js +139 -139
- package/lib/auth.js +181 -181
- package/lib/client.js +496 -496
- package/lib/code-check.js +549 -549
- package/lib/config.js +323 -323
- package/lib/contract-history.js +49 -49
- package/lib/contract.js +583 -583
- package/lib/csharp-check.js +493 -493
- package/lib/data-migrate.js +1318 -1318
- package/lib/data-policy.js +78 -78
- package/lib/error-codes.js +80 -80
- package/lib/gitlab.js +161 -161
- package/lib/health.js +205 -205
- package/lib/migrate.js +732 -732
- package/lib/preflight.js +116 -116
- package/lib/refs.js +191 -191
- package/lib/resources.js +320 -320
- package/lib/retention-defaults.js +17 -17
- package/lib/retention.js +384 -384
- package/lib/runtime.js +232 -232
- package/lib/settings-http.js +512 -512
- package/lib/settings-store.js +371 -371
- package/lib/snapshot.js +149 -149
- package/lib/tool-code-check.js +133 -133
- package/lib/tool-configure.js +169 -169
- package/lib/tool-contract.js +376 -376
- package/lib/tool-data-sync.js +559 -559
- package/lib/tool-data-write.js +203 -203
- package/lib/tool-data.js +282 -282
- package/lib/tool-diff.js +484 -484
- package/lib/tool-environments.js +106 -106
- package/lib/tool-flows.js +540 -540
- package/lib/tool-health.js +127 -127
- package/lib/tool-layers.js +497 -497
- package/lib/tool-metadata.js +417 -417
- package/lib/tool-migrate.js +102 -102
- package/lib/tool-refs.js +206 -206
- package/lib/tool-reports.js +299 -299
- package/lib/tool-sync.js +430 -430
- package/lib/tool-write.js +563 -563
- package/lib/waivers.js +83 -83
- package/lib/write-actions.js +1374 -1374
- package/lib/write-policy.js +199 -199
- package/lib/write-store.js +167 -167
- package/locale/en.json +68 -68
- package/locale/zh.json +68 -68
- package/package.json +2 -2
- package/skills/d365-troubleshooting/SKILL.md +115 -115
package/docs/en/checks.md
CHANGED
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
# Check Rules
|
|
2
|
-
|
|
3
|
-
**English** | [简体中文](../zh/checks.md)
|
|
4
|
-
|
|
5
|
-
Three check tools answer different questions:
|
|
6
|
-
|
|
7
|
-
| Tool | Question | Data source |
|
|
8
|
-
| --- | --- | --- |
|
|
9
|
-
| `d365_migrate` | What happens when this solution is imported into the target environment | Live read, layer lookup per component |
|
|
10
|
-
| `d365_contract` | Do downstream environments still honor upstream registrations and references | Live read (drift reflects the present) |
|
|
11
|
-
| `d365_code_check` | Will the code changes in this MR run in the target environment | GitLab MR + live environment read |
|
|
12
|
-
|
|
13
|
-
Shared convention: **high confidence blocks outright, medium confidence drops to a warning, anything static analysis cannot decide goes to manual confirmation** — better to have a human take one more look than to let a single false positive turn the tool into noise.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Migration assessment (d365_migrate assess)
|
|
18
|
-
|
|
19
|
-
Component-by-component comparison produces these findings:
|
|
20
|
-
|
|
21
|
-
| Rule | Criterion | Level |
|
|
22
|
-
| --- | --- | --- |
|
|
23
|
-
| `component-missing` | The component does not exist in the target environment | info |
|
|
24
|
-
| `solution-absent` | The target does not have this solution (first import) | warning |
|
|
25
|
-
| `version-not-newer` | The target version is not lower than the source version (the import does not overwrite it with a newer one) | warning |
|
|
26
|
-
| `managed-mismatch` | Managed state differs between source and target | warning |
|
|
27
|
-
| `source-unmanaged` | The source is unmanaged, so the import leaves a layer that cannot be removed by uninstalling | warning |
|
|
28
|
-
| `option-values-lost` | Option values the target has and the source lacks — deleted by the import | **critical** |
|
|
29
|
-
| `option-label-drift` | The same value carries a different label | warning |
|
|
30
|
-
| `shadowed` | Another solution owns the top layer of this component in the target, so its definition is invalidated after import | listed in summary |
|
|
31
|
-
|
|
32
|
-
### Component types and layers
|
|
33
|
-
|
|
34
|
-
Only six types can be looked up by name in the layer table:
|
|
35
|
-
|
|
36
|
-
| `msdyn_solutioncomponentname` | componenttype |
|
|
37
|
-
| --- | --- |
|
|
38
|
-
| `Entity` | 1 |
|
|
39
|
-
| `Attribute` | 2 |
|
|
40
|
-
| `OptionSet` | 9 |
|
|
41
|
-
| `EntityRelationship` | 10 |
|
|
42
|
-
| `Workflow` | 29 |
|
|
43
|
-
| `WebResource` | 61 |
|
|
44
|
-
|
|
45
|
-
The remaining types (36 reports, 50 chart/security types, 70 app types…) have no matching name in the layer table, so they only get an **existence check** and are marked "not assessed" in the report.
|
|
46
|
-
|
|
47
|
-
The keys used to identify a component across environments come from the layer record's `msdyn_componentjson`: Entity → `logicalname`, Attribute → `logicalname` + parent table, OptionSet → `name`, EntityRelationship → `schemaname`, Workflow → `name` + `category`, WebResource → `name`.
|
|
48
|
-
|
|
49
|
-
### Why it is slow
|
|
50
|
-
|
|
51
|
-
`msdyn_componentlayer` is a virtual entity: it can only be queried one component at a time by "component id + component type" and cannot be fetched in bulk per solution; a single component takes about 2 seconds. Hence `maxComponents` (default 150) and `concurrency` (default 4, max 8), and only `full: true` assesses everything.
|
|
52
|
-
|
|
53
|
-
### Reading order for the report
|
|
54
|
-
|
|
55
|
-
1. Start with **critical**: are any option values lost;
|
|
56
|
-
2. Then **shadowing**: whose definition gets invalidated by the import, and is it one of your own managed patches;
|
|
57
|
-
3. Finally **missing pieces and versions**: a component missing from the target usually means the release pipeline skipped a step;
|
|
58
|
-
4. Landing the conclusion: if the change can join a managed solution and ride along with upgrades, do not ship a separate small unmanaged solution — layers stack by import time, and the next patch that comes in covers it again.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
## Contract drift (d365_contract)
|
|
63
|
-
|
|
64
|
-
Treat upstream plugin registrations and visual component references as a **contract**, and check whether the downstream still satisfies it.
|
|
65
|
-
|
|
66
|
-
| Rule | Criterion | Level |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| `assembly-missing` | Upstream has the assembly, downstream does not | Blocker |
|
|
69
|
-
| `assembly-version-older` | Downstream version is lower than upstream | Warning |
|
|
70
|
-
| `plugin-type-missing` | A type registered upstream is missing downstream | Blocker |
|
|
71
|
-
| `plugin-step-missing` | The type exists but the step for the "message + stage + mode" combination is missing | Blocker |
|
|
72
|
-
| `form-library-missing` | A web resource referenced by a downstream form does not exist | Blocker |
|
|
73
|
-
| `sitemap-missing` | An upstream custom sitemap is missing downstream | Warning |
|
|
74
|
-
| `handler-missing` | The function named by a form handler cannot be found in the project scripts | Warning |
|
|
75
|
-
| `option-value-missing` | Downstream is missing an upstream option value (or has a value upstream lacks) | Warning |
|
|
76
|
-
|
|
77
|
-
Details:
|
|
78
|
-
|
|
79
|
-
- Only **unmanaged** assemblies are compared by default; managed assemblies come from Microsoft or from a managed solution and cannot be changed locally;
|
|
80
|
-
- Steps compare by "type + message + stage + mode": async and sync are two different contracts;
|
|
81
|
-
- Option value comparison requires passing `optionSets` explicitly (each one is read from `GlobalOptionSetDefinitions`); without it, nothing is compared;
|
|
82
|
-
- Content that exists downstream but **not** upstream is never a blocker; when an option set has extra values the only hint is "confirm whether it has been back-ported".
|
|
83
|
-
|
|
84
|
-
### Handler check
|
|
85
|
-
|
|
86
|
-
With `handlers: true`, the project scripts referenced by forms are downloaded and checked for the existence of `functionName`.
|
|
87
|
-
|
|
88
|
-
- Only **project scripts** are judged. The prefixes are inferred from the source environment's unmanaged solutions — the publisher prefix that most of them use, plus `new_` — so a fresh install works against any organization without configuration; set `handlerPrefixes` to pin the list exactly. Partner and sample solutions are deliberately left out: their bundles are minified, so their internal names are mangled and every handler would look missing (on one real environment: 94 findings instead of 14);
|
|
89
|
-
- The handler's own library is checked first; if the function is not found, the search widens to the other project libraries the form loads (form-level shared scope really exists: one library provides the function, another binds it);
|
|
90
|
-
- Platform-injected handlers (`absoluteurl_onchange`, `Main_Form_onload`, etc.) come from system scripts and are not judged;
|
|
91
|
-
- Downloading is capped (400 scripts per environment); anything beyond that is treated as "content unreadable, cannot confirm" rather than falsely reported as a blocker.
|
|
92
|
-
|
|
93
|
-
The first version judged against "every script library a form loads" and produced 306 warnings; narrowed to "project libraries, own library first" it produces 14, all of them cases where the script was read and the function genuinely is not there. **That number is the design goal: a small actionable set, not an unreadable pile.**
|
|
94
|
-
|
|
95
|
-
### Waivers and trends
|
|
96
|
-
|
|
97
|
-
```json
|
|
98
|
-
{ "action": "waive", "rule": "form-library-missing", "env": "sit",
|
|
99
|
-
"object": "/_static/_common/scripts/CommandbarActions.js", "reason": "this environment does not use SLA dialogs", "days": 30 }
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
- Stored in `.d365/waivers.json`; `env` accepts `*`, `object` accepts `*` and the prefix form `x*`;
|
|
103
|
-
- An expiry is mandatory (default 30 days); once expired the item reappears in the report automatically;
|
|
104
|
-
- The report shows "N waived", and waived items never enter the blocker list;
|
|
105
|
-
- Every drift run appends an entry to `.d365/reports/contract-history.json` (60 kept), and the next run over the same `source → targets` reports "new / gone".
|
|
106
|
-
|
|
107
|
-
### Why live data
|
|
108
|
-
|
|
109
|
-
Plugin registration scale: one environment has about 376 assemblies, 6,400 types, and 5,900 types with steps (about 94,000 step records), read in tens of seconds. Drift computed from a snapshot a few days old produces false conclusions, so no snapshots are used here.
|
|
110
|
-
|
|
111
|
-
---
|
|
112
|
-
|
|
113
|
-
## Code check (d365_code_check)
|
|
114
|
-
|
|
115
|
-
Reads the MR once and analyzes only the files it touches. Requires a read-only GitLab token (`read_api`); the token's user must be at least **Reporter**.
|
|
116
|
-
|
|
117
|
-
### JS / TS
|
|
118
|
-
|
|
119
|
-
Chain: changed file → match a web resource in the environment by **file name** (when paths differ, take the closest same-name candidate and mark it "inferred, pending confirmation" in the report) → find the forms that reference that resource → compare the changed lines one by one.
|
|
120
|
-
|
|
121
|
-
| Check | Verdict |
|
|
122
|
-
| --- | --- |
|
|
123
|
-
| A field read by an added line is on **no** form that loads the library | one blocker |
|
|
124
|
-
| The field is only on some forms (the script may never reach that branch on those forms) | warning, listing the missing forms (at most 3) |
|
|
125
|
-
| A referenced entity does not exist (form `datafieldname`, `Xrm.WebApi` entity set) | blocker |
|
|
126
|
-
| A deleted function is still referenced by a form handler | blocker |
|
|
127
|
-
| A deleted file is still referenced by a form | blocker (not reported when no form references it) |
|
|
128
|
-
| `columns.get('x')` | manual confirmation (view/subgrid column — not being a form field is normal) |
|
|
129
|
-
| A prefixed string subscript `["contoso_x"]` | manual confirmation (cannot tell statically whether it is a field or a plain key) |
|
|
130
|
-
| A newly introduced hard-coded GUID, option value, or environment URL | warning or manual confirmation |
|
|
131
|
-
| Configuration data, field-level security, the running user's privileges | manual confirmation (cannot be decided statically) |
|
|
132
|
-
|
|
133
|
-
Access patterns covered: `getAttribute`, `getControl`, `attributes.get`, `formContext.data.entity.attributes.get`, `Xrm.Page.getAttribute`, `attributes[...]`, and prefixed string subscripts.
|
|
134
|
-
Relying on a single `getAttribute` regex misses a lot of compiled output (measured: a 103 KB compiled TS script hit 0 times), so access is split into three groups: definitely a field, definitely a column, needs human judgment.
|
|
135
|
-
|
|
136
|
-
### C# / project files
|
|
137
|
-
|
|
138
|
-
Infer the assembly name from the **innermost project directory** in the path (an `<AssemblyName>` in the `.csproj` wins when present) → **exact match** against `pluginassemblies` in the environment → look up `plugintypes`, `sdkmessageprocessingsteps`, `sdkmessageprocessingstepimages`.
|
|
139
|
-
|
|
140
|
-
| Check | Verdict |
|
|
141
|
-
| --- | --- |
|
|
142
|
-
| The assembly does not exist in the target environment (shared projects such as `.Processes` fall here) | warning + manual confirmation of which assembly the file ultimately ships in |
|
|
143
|
-
| The source version differs from the environment assembly version | warning |
|
|
144
|
-
| An `IPlugin` class has no matching plugin type in the target environment | blocker |
|
|
145
|
-
| The type exists but has no steps at all (changes will never fire) | warning |
|
|
146
|
-
| Code reads `PreEntityImages["X"]` / `PostEntityImages["X"]` but the step has no such image registered | blocker (Create steps do not check pre-images) |
|
|
147
|
-
| A step's `filteringattributes` references a field that does not exist on the entity | blocker |
|
|
148
|
-
| An enumerated `contoso_*` literal is neither a table nor a global option set in the target | manual confirmation |
|
|
149
|
-
| Two or more steps on the same `message\|stage` | warning (execution order is undefined when ranks collide) |
|
|
150
|
-
| Code uses `secureConfig` / a constructor with two string parameters | manual confirmation (the Web API does not return secure configuration content) |
|
|
151
|
-
| APIs the sandbox may forbid (`System.IO`, `System.Net`, `Thread.Sleep`…) | warning |
|
|
152
|
-
|
|
153
|
-
Assembly names are matched exactly, never approximated — a wrong guess checks types and steps against the wrong assembly, which is worse than not checking at all.
|
|
154
|
-
|
|
155
|
-
### Report structure
|
|
156
|
-
|
|
157
|
-
```
|
|
158
|
-
Changed file table
|
|
159
|
-
Per-file impact (which resource/assembly it maps to, which forms/types)
|
|
160
|
-
Blockers (must fix)
|
|
161
|
-
Warnings (should fix, or register a waiver)
|
|
162
|
-
Manual confirmation (HC-01…, each entry states why a machine cannot decide)
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Known limits
|
|
166
|
-
|
|
167
|
-
- Only the MR's added and removed lines are analyzed; no other repository files are read (no import resolution, no call-chain following);
|
|
168
|
-
- Path → web resource name is inferred from the file name, and marked "inferred" when the repository path and the resource name disagree;
|
|
169
|
-
- C# filtering attributes, images, and types are judged from registration data only; secure/unsecure configuration content and implicit ordering beyond rank still need manual confirmation;
|
|
170
|
-
- The running user's privileges and the actual shape of the data are not judged.
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## Division of labor between the three checks
|
|
175
|
-
|
|
176
|
-
```
|
|
177
|
-
Code change ──d365_code_check──> can this change ship (one-off, MR-level)
|
|
178
|
-
Upstream vs downstream ──d365_contract──> is the contract between environments broken (ongoing, version-level)
|
|
179
|
-
Solution ──d365_migrate──> will this import shadow someone else (one-off, release-level)
|
|
180
|
-
Who overrode it ──d365_layers──> why this environment looks the way it does
|
|
181
|
-
Who uses it ──d365_refs──> the impact of changing or deleting it
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
They do not replace each other: `d365_layers` tells you who overrode it, `d365_refs` tells you who references it, and `d365_code_check` tells you whether the lines you changed land on something that does not exist.
|
|
1
|
+
# Check Rules
|
|
2
|
+
|
|
3
|
+
**English** | [简体中文](../zh/checks.md)
|
|
4
|
+
|
|
5
|
+
Three check tools answer different questions:
|
|
6
|
+
|
|
7
|
+
| Tool | Question | Data source |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `d365_migrate` | What happens when this solution is imported into the target environment | Live read, layer lookup per component |
|
|
10
|
+
| `d365_contract` | Do downstream environments still honor upstream registrations and references | Live read (drift reflects the present) |
|
|
11
|
+
| `d365_code_check` | Will the code changes in this MR run in the target environment | GitLab MR + live environment read |
|
|
12
|
+
|
|
13
|
+
Shared convention: **high confidence blocks outright, medium confidence drops to a warning, anything static analysis cannot decide goes to manual confirmation** — better to have a human take one more look than to let a single false positive turn the tool into noise.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Migration assessment (d365_migrate assess)
|
|
18
|
+
|
|
19
|
+
Component-by-component comparison produces these findings:
|
|
20
|
+
|
|
21
|
+
| Rule | Criterion | Level |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `component-missing` | The component does not exist in the target environment | info |
|
|
24
|
+
| `solution-absent` | The target does not have this solution (first import) | warning |
|
|
25
|
+
| `version-not-newer` | The target version is not lower than the source version (the import does not overwrite it with a newer one) | warning |
|
|
26
|
+
| `managed-mismatch` | Managed state differs between source and target | warning |
|
|
27
|
+
| `source-unmanaged` | The source is unmanaged, so the import leaves a layer that cannot be removed by uninstalling | warning |
|
|
28
|
+
| `option-values-lost` | Option values the target has and the source lacks — deleted by the import | **critical** |
|
|
29
|
+
| `option-label-drift` | The same value carries a different label | warning |
|
|
30
|
+
| `shadowed` | Another solution owns the top layer of this component in the target, so its definition is invalidated after import | listed in summary |
|
|
31
|
+
|
|
32
|
+
### Component types and layers
|
|
33
|
+
|
|
34
|
+
Only six types can be looked up by name in the layer table:
|
|
35
|
+
|
|
36
|
+
| `msdyn_solutioncomponentname` | componenttype |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `Entity` | 1 |
|
|
39
|
+
| `Attribute` | 2 |
|
|
40
|
+
| `OptionSet` | 9 |
|
|
41
|
+
| `EntityRelationship` | 10 |
|
|
42
|
+
| `Workflow` | 29 |
|
|
43
|
+
| `WebResource` | 61 |
|
|
44
|
+
|
|
45
|
+
The remaining types (36 reports, 50 chart/security types, 70 app types…) have no matching name in the layer table, so they only get an **existence check** and are marked "not assessed" in the report.
|
|
46
|
+
|
|
47
|
+
The keys used to identify a component across environments come from the layer record's `msdyn_componentjson`: Entity → `logicalname`, Attribute → `logicalname` + parent table, OptionSet → `name`, EntityRelationship → `schemaname`, Workflow → `name` + `category`, WebResource → `name`.
|
|
48
|
+
|
|
49
|
+
### Why it is slow
|
|
50
|
+
|
|
51
|
+
`msdyn_componentlayer` is a virtual entity: it can only be queried one component at a time by "component id + component type" and cannot be fetched in bulk per solution; a single component takes about 2 seconds. Hence `maxComponents` (default 150) and `concurrency` (default 4, max 8), and only `full: true` assesses everything.
|
|
52
|
+
|
|
53
|
+
### Reading order for the report
|
|
54
|
+
|
|
55
|
+
1. Start with **critical**: are any option values lost;
|
|
56
|
+
2. Then **shadowing**: whose definition gets invalidated by the import, and is it one of your own managed patches;
|
|
57
|
+
3. Finally **missing pieces and versions**: a component missing from the target usually means the release pipeline skipped a step;
|
|
58
|
+
4. Landing the conclusion: if the change can join a managed solution and ride along with upgrades, do not ship a separate small unmanaged solution — layers stack by import time, and the next patch that comes in covers it again.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Contract drift (d365_contract)
|
|
63
|
+
|
|
64
|
+
Treat upstream plugin registrations and visual component references as a **contract**, and check whether the downstream still satisfies it.
|
|
65
|
+
|
|
66
|
+
| Rule | Criterion | Level |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `assembly-missing` | Upstream has the assembly, downstream does not | Blocker |
|
|
69
|
+
| `assembly-version-older` | Downstream version is lower than upstream | Warning |
|
|
70
|
+
| `plugin-type-missing` | A type registered upstream is missing downstream | Blocker |
|
|
71
|
+
| `plugin-step-missing` | The type exists but the step for the "message + stage + mode" combination is missing | Blocker |
|
|
72
|
+
| `form-library-missing` | A web resource referenced by a downstream form does not exist | Blocker |
|
|
73
|
+
| `sitemap-missing` | An upstream custom sitemap is missing downstream | Warning |
|
|
74
|
+
| `handler-missing` | The function named by a form handler cannot be found in the project scripts | Warning |
|
|
75
|
+
| `option-value-missing` | Downstream is missing an upstream option value (or has a value upstream lacks) | Warning |
|
|
76
|
+
|
|
77
|
+
Details:
|
|
78
|
+
|
|
79
|
+
- Only **unmanaged** assemblies are compared by default; managed assemblies come from Microsoft or from a managed solution and cannot be changed locally;
|
|
80
|
+
- Steps compare by "type + message + stage + mode": async and sync are two different contracts;
|
|
81
|
+
- Option value comparison requires passing `optionSets` explicitly (each one is read from `GlobalOptionSetDefinitions`); without it, nothing is compared;
|
|
82
|
+
- Content that exists downstream but **not** upstream is never a blocker; when an option set has extra values the only hint is "confirm whether it has been back-ported".
|
|
83
|
+
|
|
84
|
+
### Handler check
|
|
85
|
+
|
|
86
|
+
With `handlers: true`, the project scripts referenced by forms are downloaded and checked for the existence of `functionName`.
|
|
87
|
+
|
|
88
|
+
- Only **project scripts** are judged. The prefixes are inferred from the source environment's unmanaged solutions — the publisher prefix that most of them use, plus `new_` — so a fresh install works against any organization without configuration; set `handlerPrefixes` to pin the list exactly. Partner and sample solutions are deliberately left out: their bundles are minified, so their internal names are mangled and every handler would look missing (on one real environment: 94 findings instead of 14);
|
|
89
|
+
- The handler's own library is checked first; if the function is not found, the search widens to the other project libraries the form loads (form-level shared scope really exists: one library provides the function, another binds it);
|
|
90
|
+
- Platform-injected handlers (`absoluteurl_onchange`, `Main_Form_onload`, etc.) come from system scripts and are not judged;
|
|
91
|
+
- Downloading is capped (400 scripts per environment); anything beyond that is treated as "content unreadable, cannot confirm" rather than falsely reported as a blocker.
|
|
92
|
+
|
|
93
|
+
The first version judged against "every script library a form loads" and produced 306 warnings; narrowed to "project libraries, own library first" it produces 14, all of them cases where the script was read and the function genuinely is not there. **That number is the design goal: a small actionable set, not an unreadable pile.**
|
|
94
|
+
|
|
95
|
+
### Waivers and trends
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "action": "waive", "rule": "form-library-missing", "env": "sit",
|
|
99
|
+
"object": "/_static/_common/scripts/CommandbarActions.js", "reason": "this environment does not use SLA dialogs", "days": 30 }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- Stored in `.d365/waivers.json`; `env` accepts `*`, `object` accepts `*` and the prefix form `x*`;
|
|
103
|
+
- An expiry is mandatory (default 30 days); once expired the item reappears in the report automatically;
|
|
104
|
+
- The report shows "N waived", and waived items never enter the blocker list;
|
|
105
|
+
- Every drift run appends an entry to `.d365/reports/contract-history.json` (60 kept), and the next run over the same `source → targets` reports "new / gone".
|
|
106
|
+
|
|
107
|
+
### Why live data
|
|
108
|
+
|
|
109
|
+
Plugin registration scale: one environment has about 376 assemblies, 6,400 types, and 5,900 types with steps (about 94,000 step records), read in tens of seconds. Drift computed from a snapshot a few days old produces false conclusions, so no snapshots are used here.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Code check (d365_code_check)
|
|
114
|
+
|
|
115
|
+
Reads the MR once and analyzes only the files it touches. Requires a read-only GitLab token (`read_api`); the token's user must be at least **Reporter**.
|
|
116
|
+
|
|
117
|
+
### JS / TS
|
|
118
|
+
|
|
119
|
+
Chain: changed file → match a web resource in the environment by **file name** (when paths differ, take the closest same-name candidate and mark it "inferred, pending confirmation" in the report) → find the forms that reference that resource → compare the changed lines one by one.
|
|
120
|
+
|
|
121
|
+
| Check | Verdict |
|
|
122
|
+
| --- | --- |
|
|
123
|
+
| A field read by an added line is on **no** form that loads the library | one blocker |
|
|
124
|
+
| The field is only on some forms (the script may never reach that branch on those forms) | warning, listing the missing forms (at most 3) |
|
|
125
|
+
| A referenced entity does not exist (form `datafieldname`, `Xrm.WebApi` entity set) | blocker |
|
|
126
|
+
| A deleted function is still referenced by a form handler | blocker |
|
|
127
|
+
| A deleted file is still referenced by a form | blocker (not reported when no form references it) |
|
|
128
|
+
| `columns.get('x')` | manual confirmation (view/subgrid column — not being a form field is normal) |
|
|
129
|
+
| A prefixed string subscript `["contoso_x"]` | manual confirmation (cannot tell statically whether it is a field or a plain key) |
|
|
130
|
+
| A newly introduced hard-coded GUID, option value, or environment URL | warning or manual confirmation |
|
|
131
|
+
| Configuration data, field-level security, the running user's privileges | manual confirmation (cannot be decided statically) |
|
|
132
|
+
|
|
133
|
+
Access patterns covered: `getAttribute`, `getControl`, `attributes.get`, `formContext.data.entity.attributes.get`, `Xrm.Page.getAttribute`, `attributes[...]`, and prefixed string subscripts.
|
|
134
|
+
Relying on a single `getAttribute` regex misses a lot of compiled output (measured: a 103 KB compiled TS script hit 0 times), so access is split into three groups: definitely a field, definitely a column, needs human judgment.
|
|
135
|
+
|
|
136
|
+
### C# / project files
|
|
137
|
+
|
|
138
|
+
Infer the assembly name from the **innermost project directory** in the path (an `<AssemblyName>` in the `.csproj` wins when present) → **exact match** against `pluginassemblies` in the environment → look up `plugintypes`, `sdkmessageprocessingsteps`, `sdkmessageprocessingstepimages`.
|
|
139
|
+
|
|
140
|
+
| Check | Verdict |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| The assembly does not exist in the target environment (shared projects such as `.Processes` fall here) | warning + manual confirmation of which assembly the file ultimately ships in |
|
|
143
|
+
| The source version differs from the environment assembly version | warning |
|
|
144
|
+
| An `IPlugin` class has no matching plugin type in the target environment | blocker |
|
|
145
|
+
| The type exists but has no steps at all (changes will never fire) | warning |
|
|
146
|
+
| Code reads `PreEntityImages["X"]` / `PostEntityImages["X"]` but the step has no such image registered | blocker (Create steps do not check pre-images) |
|
|
147
|
+
| A step's `filteringattributes` references a field that does not exist on the entity | blocker |
|
|
148
|
+
| An enumerated `contoso_*` literal is neither a table nor a global option set in the target | manual confirmation |
|
|
149
|
+
| Two or more steps on the same `message\|stage` | warning (execution order is undefined when ranks collide) |
|
|
150
|
+
| Code uses `secureConfig` / a constructor with two string parameters | manual confirmation (the Web API does not return secure configuration content) |
|
|
151
|
+
| APIs the sandbox may forbid (`System.IO`, `System.Net`, `Thread.Sleep`…) | warning |
|
|
152
|
+
|
|
153
|
+
Assembly names are matched exactly, never approximated — a wrong guess checks types and steps against the wrong assembly, which is worse than not checking at all.
|
|
154
|
+
|
|
155
|
+
### Report structure
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Changed file table
|
|
159
|
+
Per-file impact (which resource/assembly it maps to, which forms/types)
|
|
160
|
+
Blockers (must fix)
|
|
161
|
+
Warnings (should fix, or register a waiver)
|
|
162
|
+
Manual confirmation (HC-01…, each entry states why a machine cannot decide)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Known limits
|
|
166
|
+
|
|
167
|
+
- Only the MR's added and removed lines are analyzed; no other repository files are read (no import resolution, no call-chain following);
|
|
168
|
+
- Path → web resource name is inferred from the file name, and marked "inferred" when the repository path and the resource name disagree;
|
|
169
|
+
- C# filtering attributes, images, and types are judged from registration data only; secure/unsecure configuration content and implicit ordering beyond rank still need manual confirmation;
|
|
170
|
+
- The running user's privileges and the actual shape of the data are not judged.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Division of labor between the three checks
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Code change ──d365_code_check──> can this change ship (one-off, MR-level)
|
|
178
|
+
Upstream vs downstream ──d365_contract──> is the contract between environments broken (ongoing, version-level)
|
|
179
|
+
Solution ──d365_migrate──> will this import shadow someone else (one-off, release-level)
|
|
180
|
+
Who overrode it ──d365_layers──> why this environment looks the way it does
|
|
181
|
+
Who uses it ──d365_refs──> the impact of changing or deleting it
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
They do not replace each other: `d365_layers` tells you who overrode it, `d365_refs` tells you who references it, and `d365_code_check` tells you whether the lines you changed land on something that does not exist.
|