@shortlink-org/portolan 0.2.4 → 0.4.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 +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { djangoAggregateCandidates, djangoAggregateMessage } from "./django-aggregates";
|
|
2
|
+
import type { DjangoAggregateCandidates } from "./django-aggregates";
|
|
3
|
+
|
|
4
|
+
export type WarningSeverity = "error" | "warning" | "info";
|
|
5
|
+
|
|
6
|
+
export interface RawWarning {
|
|
7
|
+
plugin: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WarningDiagnostic extends RawWarning {
|
|
12
|
+
aggregateCandidates?: DjangoAggregateCandidates;
|
|
13
|
+
rule: string;
|
|
14
|
+
severity: WarningSeverity;
|
|
15
|
+
action: string;
|
|
16
|
+
count?: number;
|
|
17
|
+
project?: string;
|
|
18
|
+
phase?: string;
|
|
19
|
+
ref?: string;
|
|
20
|
+
suppressed: boolean;
|
|
21
|
+
suppressionReason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface WarningGroup {
|
|
25
|
+
plugin: string;
|
|
26
|
+
rule: string;
|
|
27
|
+
severity: WarningSeverity;
|
|
28
|
+
action: string;
|
|
29
|
+
messages: WarningDiagnostic[];
|
|
30
|
+
count: number;
|
|
31
|
+
suppressed: boolean;
|
|
32
|
+
suppressionReason?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RuleDefinition {
|
|
36
|
+
id: string;
|
|
37
|
+
matches: RegExp;
|
|
38
|
+
severity: WarningSeverity;
|
|
39
|
+
action: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Rules describe the recurring limitation, not the source-specific value in a
|
|
43
|
+
// message. Their ids are therefore stable enough to use in portolan.json.
|
|
44
|
+
const RULES: RuleDefinition[] = [
|
|
45
|
+
{
|
|
46
|
+
id: "openapi.missing-operation-id",
|
|
47
|
+
matches: /\bno operationId\b/i,
|
|
48
|
+
severity: "warning",
|
|
49
|
+
action: "Add stable operationId values to the OpenAPI operations.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "schema.duplicate-declaration",
|
|
53
|
+
matches: /\bduplicate declaration\b/i,
|
|
54
|
+
severity: "warning",
|
|
55
|
+
action: "Remove or reconcile duplicate schema declarations; Portolan currently uses the first.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "analysis.typed-fallback",
|
|
59
|
+
matches: /\btyped call graph unavailable\b/i,
|
|
60
|
+
severity: "warning",
|
|
61
|
+
action: "Run an extractor build with typed analysis support or inspect calls found by the syntax fallback.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "catalog.unresolved-call",
|
|
65
|
+
matches: /\bcalls .+ which nothing in this catalog resolves\b/i,
|
|
66
|
+
severity: "warning",
|
|
67
|
+
action: "Add or correct the provider contract, then regenerate to resolve the call.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "catalog.unmapped-proto-peer",
|
|
71
|
+
matches: /\bmanifest names no peer for (?:that package|it)\b/i,
|
|
72
|
+
severity: "warning",
|
|
73
|
+
action: "Map the protobuf package under peers, or declare it under externals.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "flow.unresolved-step",
|
|
77
|
+
matches: /\bstep .+ is unresolved\b/i,
|
|
78
|
+
severity: "warning",
|
|
79
|
+
action: "Declare the referenced endpoint, message, or store so this flow step can be joined.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "flow.unknown-port",
|
|
83
|
+
matches: /\bis neither a domain port(?:,| nor) a use case\b/i,
|
|
84
|
+
severity: "warning",
|
|
85
|
+
action: "Model this dependency as a domain port or use case, or accept that its calls stay outside the flow.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "flow.unreached-event",
|
|
89
|
+
matches: /\bno flow reaches this event\b/i,
|
|
90
|
+
severity: "warning",
|
|
91
|
+
action: "Connect the event to its publisher flow or remove the stale event declaration.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "flow.unknown-event",
|
|
95
|
+
matches: /\breacts to the message named .+ which no event .+ declares is called\b/i,
|
|
96
|
+
severity: "warning",
|
|
97
|
+
action: "Declare the event name used by the handler or correct the handler mapping.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "django.invalid-aggregate-root",
|
|
101
|
+
matches: /\baggregates names .+ and no model there is called that\b/i,
|
|
102
|
+
severity: "warning",
|
|
103
|
+
action: "Choose an existing concrete model for the application's aggregates option.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: "django.unknown-http-verb",
|
|
107
|
+
matches: /\bmounted as an HTTP view, but no HTTP verb is declared\b/i,
|
|
108
|
+
severity: "warning",
|
|
109
|
+
action: "Declare the accepted HTTP methods on the view or route.",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "river.missing-worker",
|
|
113
|
+
matches: /\bno registered Worker\b/i,
|
|
114
|
+
severity: "warning",
|
|
115
|
+
action: "Register the River worker in this component or remove the unmatched insert.",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "watermill.unresolved-topic",
|
|
119
|
+
matches: /\bWatermill .+\btopic (?:generator could not be resolved|unresolved)\b/i,
|
|
120
|
+
severity: "warning",
|
|
121
|
+
action: "Use a literal, constant, or configuration default for the Watermill topic.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "messaging.unresolved-subject",
|
|
125
|
+
matches: /\bsubject of .+ could not be resolved\b/i,
|
|
126
|
+
severity: "warning",
|
|
127
|
+
action: "Use a literal, constant, configuration default, or visible caller argument for the subject.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "store.external-migrations",
|
|
131
|
+
matches: /\bmigrations are applied from .+ whose schema is not in this tree\b/i,
|
|
132
|
+
severity: "warning",
|
|
133
|
+
action: "Vendor or expose the external migrations so their tables can be included in the store.",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "store.missing-foreign-table",
|
|
137
|
+
matches: /\bcolumn .+ references .+ which no migration here creates\b/i,
|
|
138
|
+
severity: "warning",
|
|
139
|
+
action: "Include the referenced table migration or correct the foreign-key target.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "schema.unresolved-type",
|
|
143
|
+
matches: /\bis not declared in the protos read here\b/i,
|
|
144
|
+
severity: "warning",
|
|
145
|
+
action: "Include the imported protobuf declaration or map the type to an external schema.",
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: "source.offline-cache",
|
|
149
|
+
matches: /\bnot (?:fetched|read) \(offline\)/i,
|
|
150
|
+
severity: "info",
|
|
151
|
+
action: "Regenerate with network access when the vendored copy must be refreshed.",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "source.unreachable",
|
|
155
|
+
matches: /\bnot (?:fetched|read) \((?!offline\))/i,
|
|
156
|
+
severity: "warning",
|
|
157
|
+
action: "The far end could not be reached and the committed copy was used; check the server and the credential, then regenerate.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: "source.unpinned",
|
|
161
|
+
matches: /\bnot pinned\b/i,
|
|
162
|
+
severity: "warning",
|
|
163
|
+
action: "Pin the source to an immutable commit or schema version.",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "source.parse-failed",
|
|
167
|
+
matches: /\bcould not (?:be read|parse|be parsed|be encoded)\b/i,
|
|
168
|
+
severity: "error",
|
|
169
|
+
action: "Open the referenced source and fix the parse or read error.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "extraction.no-match",
|
|
173
|
+
matches: /\b(?:no .+ (?:was|were) found|no .+ matched|declares no |no models in this application)\b/i,
|
|
174
|
+
severity: "info",
|
|
175
|
+
action: "Confirm this capability is absent, or point the extractor at the source that declares it.",
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
const FALLBACK_ACTION = "Inspect the referenced source and either fix the extraction gap or add a reviewed CEL policy with a reason.";
|
|
180
|
+
|
|
181
|
+
export function warningDiagnostic(
|
|
182
|
+
warning: RawWarning,
|
|
183
|
+
): WarningDiagnostic {
|
|
184
|
+
const definition = RULES.find((candidate) => candidate.matches.test(warning.message));
|
|
185
|
+
const rule = definition?.id ?? `plugin.${slug(warning.plugin)}.other-${fingerprint(warning.message)}`;
|
|
186
|
+
const ref = warningRef(warning.message);
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
...warning,
|
|
190
|
+
message: djangoAggregateMessage(warning.message),
|
|
191
|
+
...(djangoAggregateCandidates(warning.message) ? { aggregateCandidates: djangoAggregateCandidates(warning.message)! } : {}),
|
|
192
|
+
rule,
|
|
193
|
+
severity: definition?.severity ?? "warning",
|
|
194
|
+
action: definition?.action ?? FALLBACK_ACTION,
|
|
195
|
+
...(ref ? { ref } : {}),
|
|
196
|
+
suppressed: false,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function groupWarnings(
|
|
201
|
+
warnings: RawWarning[],
|
|
202
|
+
): WarningGroup[] {
|
|
203
|
+
return groupDiagnostics(warnings.map((warning) => warningDiagnostic(warning)));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function groupDiagnostics(diagnostics: WarningDiagnostic[]): WarningGroup[] {
|
|
207
|
+
const groups = new Map<string, WarningGroup>();
|
|
208
|
+
for (const warning of diagnostics) {
|
|
209
|
+
const key = `${warning.plugin}\0${warning.rule}\0${warning.severity}\0${warning.suppressed}`;
|
|
210
|
+
const existing = groups.get(key);
|
|
211
|
+
if (existing) {
|
|
212
|
+
existing.messages.push(warning);
|
|
213
|
+
existing.count += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
groups.set(key, {
|
|
217
|
+
plugin: warning.plugin,
|
|
218
|
+
rule: warning.rule,
|
|
219
|
+
severity: warning.severity,
|
|
220
|
+
action: warning.action,
|
|
221
|
+
messages: [warning],
|
|
222
|
+
count: 1,
|
|
223
|
+
suppressed: warning.suppressed,
|
|
224
|
+
...(warning.suppressionReason ? { suppressionReason: warning.suppressionReason } : {}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const order: Record<WarningSeverity, number> = { error: 0, warning: 1, info: 2 };
|
|
228
|
+
return [...groups.values()].sort(
|
|
229
|
+
(left, right) => order[left.severity] - order[right.severity]
|
|
230
|
+
|| Number(left.suppressed) - Number(right.suppressed)
|
|
231
|
+
|| right.count - left.count
|
|
232
|
+
|| left.plugin.localeCompare(right.plugin)
|
|
233
|
+
|| left.rule.localeCompare(right.rule),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function warningRef(message: string): string | undefined {
|
|
238
|
+
const match = message.match(/^(.+?):\s+(?=[A-Za-z/])/);
|
|
239
|
+
return match?.[1]?.trim() || undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function slug(value: string): string {
|
|
243
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function fingerprint(message: string): string {
|
|
247
|
+
const ref = warningRef(message);
|
|
248
|
+
const body = (ref ? message.slice(message.indexOf(":", ref.length) + 1) : message)
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.replace(/`[^`]*`|"[^"]*"|'[^']*'/g, "<value>")
|
|
251
|
+
.replace(/\b\d+\b/g, "#")
|
|
252
|
+
.replace(/\s+/g, " ")
|
|
253
|
+
.trim();
|
|
254
|
+
let hash = 0x811c9dc5;
|
|
255
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
256
|
+
hash ^= body.charCodeAt(index);
|
|
257
|
+
hash = Math.imul(hash, 0x01000193);
|
|
258
|
+
}
|
|
259
|
+
return (hash >>> 0).toString(36);
|
|
260
|
+
}
|
package/src/likec4/ids.test.ts
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
CONTAINERS_VIEW,
|
|
10
10
|
LANDSCAPE_VIEW,
|
|
11
11
|
allViewIds,
|
|
12
|
+
deployedServices,
|
|
13
|
+
environmentsOf,
|
|
12
14
|
contextViewId,
|
|
13
15
|
eventFqn,
|
|
14
16
|
flowCrossViewId,
|
|
@@ -100,14 +102,16 @@ describe("view ids", () => {
|
|
|
100
102
|
expect(new Set(ids).size).toBe(ids.length);
|
|
101
103
|
});
|
|
102
104
|
|
|
103
|
-
it("covers the landscape, the containers, every context, both views of every service
|
|
105
|
+
it("covers the landscape, the containers, every context, both views of every service, both of every flow, every environment and every deployed service", () => {
|
|
104
106
|
const ids = allViewIds(catalog);
|
|
105
107
|
const services = catalog.contexts.flatMap((c) => c.services);
|
|
106
108
|
expect(ids).toHaveLength(
|
|
107
109
|
2 +
|
|
108
110
|
catalog.contexts.length +
|
|
109
111
|
services.length * 2 +
|
|
110
|
-
catalog.flows.length * 2
|
|
112
|
+
catalog.flows.length * 2 +
|
|
113
|
+
environmentsOf(catalog).length +
|
|
114
|
+
deployedServices(catalog).length,
|
|
111
115
|
);
|
|
112
116
|
// The three C4 levels: the estate, its containers, a context, and a
|
|
113
117
|
// service opened up.
|
package/src/likec4/ids.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
Participant,
|
|
10
10
|
Service,
|
|
11
11
|
} from "../catalog";
|
|
12
|
+
import { allDeployments, deploys, environmentOf } from "../catalog";
|
|
12
13
|
import reserved from "./reserved.json";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -97,6 +98,42 @@ export const flowViewId = (flow: Flow | string): string =>
|
|
|
97
98
|
export const flowCrossViewId = (flow: Flow | string): string =>
|
|
98
99
|
`${flowViewId(flow)}_cross`;
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Where the services run, one environment at a time: the clusters and
|
|
103
|
+
* namespaces of the environment as frames, an instance of every service the
|
|
104
|
+
* snapshot places there, and between the instances what the model declares
|
|
105
|
+
* between their services (portolan.0012).
|
|
106
|
+
*/
|
|
107
|
+
export const deploymentViewId = (environment: string): string =>
|
|
108
|
+
`deploy_${safeId(environment)}`;
|
|
109
|
+
|
|
110
|
+
/** One service, every place it runs: its instances, with the frames around each. */
|
|
111
|
+
export const serviceDeployViewId = (service: Service | string): string =>
|
|
112
|
+
`deploy_svc_${safeId(typeof service === "string" ? service : service.id)}`;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The environments the snapshot places services in, sorted. A deployment of
|
|
116
|
+
* a repository no service claims places nothing and names no environment
|
|
117
|
+
* here: an environment with nothing of the estate in it is not a picture.
|
|
118
|
+
*/
|
|
119
|
+
export function environmentsOf(catalog: Catalog): string[] {
|
|
120
|
+
const services = catalog.contexts.flatMap((context) => context.services);
|
|
121
|
+
const found = new Set<string>();
|
|
122
|
+
for (const deployment of allDeployments(catalog)) {
|
|
123
|
+
if (!services.some((service) => deploys(deployment, service))) continue;
|
|
124
|
+
found.add(environmentOf(deployment));
|
|
125
|
+
}
|
|
126
|
+
return [...found].sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The services the snapshot places somewhere, in catalog order. */
|
|
130
|
+
export function deployedServices(catalog: Catalog): Service[] {
|
|
131
|
+
const deployments = allDeployments(catalog);
|
|
132
|
+
return catalog.contexts
|
|
133
|
+
.flatMap((context) => context.services)
|
|
134
|
+
.filter((service) => deployments.some((d) => deploys(d, service)));
|
|
135
|
+
}
|
|
136
|
+
|
|
100
137
|
/** Every view id the generator is expected to emit, in a stable order. */
|
|
101
138
|
export function allViewIds(catalog: Catalog): string[] {
|
|
102
139
|
const out: string[] = [LANDSCAPE_VIEW, CONTAINERS_VIEW];
|
|
@@ -111,5 +148,11 @@ export function allViewIds(catalog: Catalog): string[] {
|
|
|
111
148
|
out.push(flowViewId(flow));
|
|
112
149
|
out.push(flowCrossViewId(flow));
|
|
113
150
|
}
|
|
151
|
+
for (const environment of environmentsOf(catalog)) {
|
|
152
|
+
out.push(deploymentViewId(environment));
|
|
153
|
+
}
|
|
154
|
+
for (const service of deployedServices(catalog)) {
|
|
155
|
+
out.push(serviceDeployViewId(service));
|
|
156
|
+
}
|
|
114
157
|
return out;
|
|
115
158
|
}
|
package/src/main.tsx
CHANGED
|
@@ -3,6 +3,29 @@ import { createRoot } from "react-dom/client";
|
|
|
3
3
|
import "./index.css";
|
|
4
4
|
import { App } from "./app/App";
|
|
5
5
|
|
|
6
|
+
// A deploy replaces every hashed chunk at once, but a static host may keep
|
|
7
|
+
// serving the old index.html from cache for a while (GitLab Pages caches every
|
|
8
|
+
// file for ten minutes and lets a project set no headers of its own). A tab
|
|
9
|
+
// that opened on the old page then asks for chunks that no longer exist, the
|
|
10
|
+
// lazy import fails, and the reader sees an empty canvas. Vite reports that
|
|
11
|
+
// failure here before it throws; one reload revalidates the document and
|
|
12
|
+
// picks up the new names. The guard is keyed by the chunk that failed, so a
|
|
13
|
+
// chunk that is truly missing cannot reload the page forever.
|
|
14
|
+
const RELOADED = "portolan:reloaded-for";
|
|
15
|
+
window.addEventListener("vite:preloadError", (event) => {
|
|
16
|
+
const failed = String((event as Event & { payload?: unknown }).payload ?? "");
|
|
17
|
+
let already = "";
|
|
18
|
+
try {
|
|
19
|
+
already = sessionStorage.getItem(RELOADED) ?? "";
|
|
20
|
+
sessionStorage.setItem(RELOADED, failed);
|
|
21
|
+
} catch {
|
|
22
|
+
// Storage may be unavailable; a single reload is still worth trying.
|
|
23
|
+
}
|
|
24
|
+
if (already === failed) return;
|
|
25
|
+
event.preventDefault();
|
|
26
|
+
window.location.reload();
|
|
27
|
+
});
|
|
28
|
+
|
|
6
29
|
const container = document.getElementById("root");
|
|
7
30
|
if (!container) throw new Error("#root is missing from index.html");
|
|
8
31
|
|
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
// status among everything the line carries: a relationship is known no better
|
|
17
17
|
// than its least-known hop.
|
|
18
18
|
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
useCallback,
|
|
21
|
+
useEffect,
|
|
22
|
+
useLayoutEffect,
|
|
23
|
+
useMemo,
|
|
24
|
+
useRef,
|
|
25
|
+
useState,
|
|
26
|
+
} from "react";
|
|
20
27
|
import { useNavigate } from "react-router";
|
|
21
28
|
import {
|
|
22
29
|
Background,
|
|
@@ -79,6 +86,36 @@ export interface ContextMapGraphProps {
|
|
|
79
86
|
inactiveNodeOpacity?: number;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// React Flow reads its parent box during mount. The page shell and embedded
|
|
90
|
+
// hero can both briefly be 0×0 while their layout settles, so keep the flow
|
|
91
|
+
// out of that box until a real size has been observed.
|
|
92
|
+
function useMeasuredCanvas() {
|
|
93
|
+
const canvasRef = useRef<HTMLDivElement>(null);
|
|
94
|
+
const [hasSize, setHasSize] = useState(false);
|
|
95
|
+
|
|
96
|
+
useLayoutEffect(() => {
|
|
97
|
+
const canvas = canvasRef.current;
|
|
98
|
+
if (!canvas) return;
|
|
99
|
+
|
|
100
|
+
const measure = (): void => {
|
|
101
|
+
const next = canvas.clientWidth > 0 && canvas.clientHeight > 0;
|
|
102
|
+
setHasSize((current) => (current === next ? current : next));
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
measure();
|
|
106
|
+
if (typeof ResizeObserver === "undefined") {
|
|
107
|
+
window.addEventListener("resize", measure);
|
|
108
|
+
return () => window.removeEventListener("resize", measure);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const observer = new ResizeObserver(measure);
|
|
112
|
+
observer.observe(canvas);
|
|
113
|
+
return () => observer.disconnect();
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
return { canvasRef, hasSize };
|
|
117
|
+
}
|
|
118
|
+
|
|
82
119
|
export function ContextMapGraph({
|
|
83
120
|
catalog,
|
|
84
121
|
relations,
|
|
@@ -97,6 +134,7 @@ export function ContextMapGraph({
|
|
|
97
134
|
edges: [],
|
|
98
135
|
ready: false,
|
|
99
136
|
});
|
|
137
|
+
const { canvasRef, hasSize } = useMeasuredCanvas();
|
|
100
138
|
|
|
101
139
|
// One entry per pair the catalog has anything to say about. `back` is what
|
|
102
140
|
// turns an arrow into a double-headed one; elk only ever sees a -> b, so the
|
|
@@ -257,37 +295,43 @@ export function ContextMapGraph({
|
|
|
257
295
|
);
|
|
258
296
|
|
|
259
297
|
return (
|
|
260
|
-
<div
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
298
|
+
<div
|
|
299
|
+
ref={canvasRef}
|
|
300
|
+
className="canvas-motion relative h-full w-full"
|
|
301
|
+
>
|
|
302
|
+
{drawn.ready && hasSize ? (
|
|
303
|
+
<ReactFlow
|
|
304
|
+
nodes={shownNodes}
|
|
305
|
+
edges={shownEdges}
|
|
306
|
+
nodeTypes={contextNodeTypes}
|
|
307
|
+
edgeTypes={mapEdgeTypes}
|
|
308
|
+
onNodeClick={onNodeClick}
|
|
309
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
310
|
+
onPaneClick={() => clear("diagram")}
|
|
311
|
+
nodesDraggable={false}
|
|
312
|
+
nodesConnectable={false}
|
|
313
|
+
elementsSelectable
|
|
314
|
+
proOptions={{ hideAttribution: true }}
|
|
315
|
+
fitView
|
|
316
|
+
fitViewOptions={{ padding: compactLayout ? 0.06 : 0.16 }}
|
|
317
|
+
zoomOnScroll={zoomOnScroll}
|
|
318
|
+
preventScrolling={zoomOnScroll}
|
|
319
|
+
minZoom={0.2}
|
|
320
|
+
maxZoom={1.6}
|
|
321
|
+
key={fitKey}
|
|
322
|
+
>
|
|
323
|
+
<Background gap={20} size={2} />
|
|
324
|
+
{/* The map has no modes to switch, so its only control is the way
|
|
325
|
+
out: the drawing as a file, for the page that explains it. */}
|
|
326
|
+
{showExport ? (
|
|
327
|
+
<Panel position="top-right">
|
|
328
|
+
<ExportSeg name="context-map" />
|
|
329
|
+
</Panel>
|
|
330
|
+
) : null}
|
|
331
|
+
</ReactFlow>
|
|
332
|
+
) : (
|
|
333
|
+
<DiagramSkeleton />
|
|
334
|
+
)}
|
|
291
335
|
</div>
|
|
292
336
|
);
|
|
293
337
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { Deployment } from "./catalog";
|
|
3
|
+
import { mergeCatalogs } from "./merge";
|
|
4
|
+
import type { CatalogSource } from "./merge";
|
|
5
|
+
|
|
6
|
+
function source(path: string, deployments: Deployment[]): CatalogSource {
|
|
7
|
+
return {
|
|
8
|
+
path,
|
|
9
|
+
catalog: {
|
|
10
|
+
generatedAt: "",
|
|
11
|
+
commit: "",
|
|
12
|
+
contexts: [],
|
|
13
|
+
defs: {},
|
|
14
|
+
flows: [],
|
|
15
|
+
adrs: [],
|
|
16
|
+
deployments,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const manifest = (over: Partial<Deployment> = {}): Deployment => ({
|
|
22
|
+
id: "argocd/shop-cart-prod",
|
|
23
|
+
name: "shop-cart-prod",
|
|
24
|
+
project: "shop",
|
|
25
|
+
environment: "prod",
|
|
26
|
+
cluster: "in-cluster",
|
|
27
|
+
namespace: "shop",
|
|
28
|
+
repo: "github.com/acme/gitops",
|
|
29
|
+
path: "envs/prod/shop/cart",
|
|
30
|
+
targetRevision: "main",
|
|
31
|
+
revision: "",
|
|
32
|
+
tool: "kustomize",
|
|
33
|
+
url: "",
|
|
34
|
+
service: "shop.cart",
|
|
35
|
+
images: ["ghcr.io/acme/cart:2.1.0"],
|
|
36
|
+
basis: "manifest",
|
|
37
|
+
...over,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const api = (over: Partial<Deployment> = {}): Deployment => ({
|
|
41
|
+
id: "argocd/shop-cart-prod",
|
|
42
|
+
name: "shop-cart-prod",
|
|
43
|
+
project: "shop",
|
|
44
|
+
environment: "prod",
|
|
45
|
+
cluster: "in-cluster",
|
|
46
|
+
namespace: "shop",
|
|
47
|
+
repo: "github.com/acme/gitops",
|
|
48
|
+
path: "envs/prod/shop/cart",
|
|
49
|
+
targetRevision: "main",
|
|
50
|
+
revision: "a".repeat(40),
|
|
51
|
+
tool: "kustomize",
|
|
52
|
+
url: "https://argocd.example.com/applications/argocd/shop-cart-prod",
|
|
53
|
+
images: ["ghcr.io/acme/cart:2.1.0", "redis:7"],
|
|
54
|
+
basis: "api",
|
|
55
|
+
...over,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("the merge lays the tree under the deployer", () => {
|
|
59
|
+
it("folds a manifest row and an api row into one that the deployer shapes and the tree fills, with no drift when they agree", () => {
|
|
60
|
+
const { catalog, conflicts } = mergeCatalogs([
|
|
61
|
+
source("gitops/portolan/argocd.json", [manifest()]),
|
|
62
|
+
source("argocd/argocd.apps.json", [api({ service: undefined })]),
|
|
63
|
+
]);
|
|
64
|
+
expect(conflicts).toEqual([]);
|
|
65
|
+
expect(catalog.deployments).toHaveLength(1);
|
|
66
|
+
const [row] = catalog.deployments!;
|
|
67
|
+
expect(row).toMatchObject({
|
|
68
|
+
basis: "both",
|
|
69
|
+
revision: "a".repeat(40),
|
|
70
|
+
url: "https://argocd.example.com/applications/argocd/shop-cart-prod",
|
|
71
|
+
// The labels the tree carries place the row when the deployer's did not.
|
|
72
|
+
service: "shop.cart",
|
|
73
|
+
// What runs, not what is pinned: the base's redis is running too.
|
|
74
|
+
images: ["ghcr.io/acme/cart:2.1.0", "redis:7"],
|
|
75
|
+
});
|
|
76
|
+
expect(row?.drift).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("keeps the tree's word as drift where the deployer says otherwise, whichever arrives first", () => {
|
|
80
|
+
const tree = manifest({ targetRevision: "release-2.1", images: ["ghcr.io/acme/cart:2.1.0"], namespace: "shop" });
|
|
81
|
+
const cluster = api({ targetRevision: "main", images: ["ghcr.io/acme/cart:2.0.9"], namespace: "shop-old" });
|
|
82
|
+
for (const order of [[tree, cluster], [cluster, tree]]) {
|
|
83
|
+
const { catalog, conflicts } = mergeCatalogs([
|
|
84
|
+
source("first.json", [order[0]!]),
|
|
85
|
+
source("second.json", [order[1]!]),
|
|
86
|
+
]);
|
|
87
|
+
expect(conflicts).toEqual([]);
|
|
88
|
+
const [row] = catalog.deployments!;
|
|
89
|
+
expect(row?.basis).toBe("both");
|
|
90
|
+
expect(row?.targetRevision).toBe("main");
|
|
91
|
+
expect(row?.namespace).toBe("shop-old");
|
|
92
|
+
expect(row?.drift).toEqual({
|
|
93
|
+
namespace: "shop",
|
|
94
|
+
targetRevision: "release-2.1",
|
|
95
|
+
images: ["ghcr.io/acme/cart:2.1.0"],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("does not call a pinned image drift when nothing runs yet, and shows the pinned ones instead", () => {
|
|
101
|
+
const { catalog } = mergeCatalogs([
|
|
102
|
+
source("tree.json", [manifest()]),
|
|
103
|
+
source("api.json", [api({ images: undefined, revision: "" })]),
|
|
104
|
+
]);
|
|
105
|
+
const [row] = catalog.deployments!;
|
|
106
|
+
expect(row?.images).toEqual(["ghcr.io/acme/cart:2.1.0"]);
|
|
107
|
+
expect(row?.drift).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("leaves a row only one source spoke for as it was, and reports two of one basis as a collision", () => {
|
|
111
|
+
// Sources are read in path order, so the names say who comes first.
|
|
112
|
+
const { catalog, conflicts } = mergeCatalogs([
|
|
113
|
+
source("a-tree.json", [manifest()]),
|
|
114
|
+
source("b-tree.json", [manifest({ path: "elsewhere" })]),
|
|
115
|
+
source("c-api.json", [api()]),
|
|
116
|
+
source("d-api.json", [api({ revision: "b".repeat(40) })]),
|
|
117
|
+
]);
|
|
118
|
+
expect(catalog.deployments).toHaveLength(1);
|
|
119
|
+
expect(catalog.deployments?.[0]?.revision).toBe("a".repeat(40));
|
|
120
|
+
expect(conflicts.map((c) => c.path)).toEqual(["b-tree.json", "d-api.json"]);
|
|
121
|
+
expect(conflicts[0]?.message).toContain("declared here and in a-tree.json");
|
|
122
|
+
expect(conflicts[1]?.message).toContain("listed here and in c-api.json");
|
|
123
|
+
|
|
124
|
+
const alone = mergeCatalogs([source("tree.json", [manifest()])]);
|
|
125
|
+
expect(alone.catalog.deployments?.[0]).toEqual(manifest());
|
|
126
|
+
});
|
|
127
|
+
});
|