@intentius/chant 0.124.32 → 0.124.33
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/dist/ci-jobs.d.ts +4 -0
- package/dist/ci-jobs.d.ts.map +1 -1
- package/dist/ci-jobs.js +9 -2
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -3
- package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
- package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-health.js +150 -0
- package/dist/cli/handlers/lifecycle.d.ts +26 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +1 -1
- package/dist/cli/handlers/pr.d.ts +2 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +124 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +16 -1
- package/dist/cli/handlers/release.d.ts +16 -1
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +83 -11
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +81 -21
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +23 -1
- package/dist/cli/registry.d.ts +18 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/deploy-inputs.d.ts +91 -0
- package/dist/components/deploy-inputs.d.ts.map +1 -0
- package/dist/components/deploy-inputs.js +184 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +9 -0
- package/dist/components/gate-plan.d.ts +8 -0
- package/dist/components/gate-plan.d.ts.map +1 -1
- package/dist/components/gate-plan.js +6 -0
- package/dist/components/pr-pipeline.d.ts +33 -10
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +42 -11
- package/dist/components/pr-run.d.ts +1 -1
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/release-compile.d.ts +37 -2
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +64 -8
- package/dist/components/release-promote.d.ts +17 -3
- package/dist/components/release-promote.d.ts.map +1 -1
- package/dist/components/release-promote.js +79 -18
- package/dist/components/verbs/bom-writer.d.ts +1 -1
- package/dist/components/verbs/bom-writer.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
- package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
- package/dist/components/wave-plan.d.ts +10 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +32 -4
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-detail.js +32 -7
- package/dist/health.d.ts +110 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +105 -0
- package/dist/health.schema.json +191 -0
- package/dist/lexicon.d.ts +27 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/merge-record.d.ts +104 -0
- package/dist/lifecycle/merge-record.d.ts.map +1 -0
- package/dist/lifecycle/merge-record.js +102 -0
- package/dist/lifecycle/publish-attachments.d.ts +8 -5
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
- package/dist/lifecycle/publish-attachments.js +27 -13
- package/dist/lifecycle/publish-npm.d.ts +3 -2
- package/dist/lifecycle/publish-npm.d.ts.map +1 -1
- package/dist/lifecycle/publish-npm.js +4 -3
- package/dist/lifecycle/release-at.d.ts +40 -0
- package/dist/lifecycle/release-at.d.ts.map +1 -0
- package/dist/lifecycle/release-at.js +83 -0
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +23 -12
- package/dist/lifecycle/release-publish.d.ts +28 -12
- package/dist/lifecycle/release-publish.d.ts.map +1 -1
- package/dist/lifecycle/release-publish.js +29 -17
- package/dist/lifecycle/release-store.d.ts +25 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +40 -13
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +1 -0
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.js +4 -2
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
- package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
- package/dist/op/activities/activity-contracts.d.ts +5 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +3 -0
- package/dist/op/activities/shell.d.ts +38 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +89 -3
- package/dist/op/builders.d.ts +7 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/builders.js +7 -0
- package/dist/op/runtime.d.ts +2 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +14 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +15 -5
- package/dist/pr-report-url.d.ts +2 -2
- package/dist/pr-report-url.d.ts.map +1 -1
- package/dist/runner-start.d.ts +179 -0
- package/dist/runner-start.d.ts.map +1 -0
- package/dist/runner-start.js +277 -0
- package/dist/wave-runtime.mjs +235 -228
- package/dist/workspace/check.schema.json +70 -2
- package/dist/workspace/checks/security.d.ts +35 -0
- package/dist/workspace/checks/security.d.ts.map +1 -0
- package/dist/workspace/checks/security.js +305 -0
- package/dist/workspace/checks.d.ts +2 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +5 -0
- package/dist/workspace/ci-builds.d.ts +163 -0
- package/dist/workspace/ci-builds.d.ts.map +1 -0
- package/dist/workspace/ci-builds.js +315 -0
- package/dist/workspace/ci-green-cli.d.ts +2 -1
- package/dist/workspace/ci-green-cli.d.ts.map +1 -1
- package/dist/workspace/ci-green-cli.js +25 -5
- package/dist/workspace/ci-green-forge.d.ts +12 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green-forge.js +12 -0
- package/dist/workspace/ci-green-workflow.d.ts +55 -1
- package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
- package/dist/workspace/ci-green-workflow.js +128 -1
- package/dist/workspace/ci-green.d.ts +89 -4
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +310 -23
- package/dist/workspace/ci-last-green.schema.json +49 -0
- package/dist/workspace/ci-list.schema.json +35 -1
- package/dist/workspace/ci-run-index.d.ts +1 -1
- package/dist/workspace/ci-run-index.d.ts.map +1 -1
- package/dist/workspace/ci-run-index.schema.json +1 -1
- package/dist/workspace/ci-run.d.ts +12 -6
- package/dist/workspace/ci-run.d.ts.map +1 -1
- package/dist/workspace/ci-run.js +72 -16
- package/dist/workspace/ci-runner-start.d.ts +77 -0
- package/dist/workspace/ci-runner-start.d.ts.map +1 -0
- package/dist/workspace/ci-runner-start.js +185 -0
- package/dist/workspace/ci-serve.d.ts +76 -7
- package/dist/workspace/ci-serve.d.ts.map +1 -1
- package/dist/workspace/ci-serve.js +153 -19
- package/dist/workspace/declaration.d.ts +17 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +3 -0
- package/dist/workspace/declaration.schema.json +23 -0
- package/dist/workspace/ls.d.ts +2 -0
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/ls.js +1 -1
- package/dist/workspace/ls.schema.json +2 -1
- package/dist/workspace/member-pipeline.d.ts +1 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.js +2 -0
- package/dist/workspace/merge-record.schema.json +49 -0
- package/dist/workspace/pr-report.schema.json +3 -3
- package/dist/workspace/reason-codes.d.ts +17 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +18 -0
- package/dist/workspace/release-candidates.d.ts +129 -0
- package/dist/workspace/release-candidates.d.ts.map +1 -0
- package/dist/workspace/release-candidates.js +233 -0
- package/dist/workspace/release-candidates.schema.json +353 -0
- package/dist/workspace/security.d.ts +40 -0
- package/dist/workspace/security.d.ts.map +1 -0
- package/dist/workspace/security.js +99 -0
- package/package.json +8 -1
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
- package/src/ci-jobs.ts +11 -2
- package/src/cli/handlers/build.test.ts +36 -0
- package/src/cli/handlers/build.ts +18 -3
- package/src/cli/handlers/lifecycle-health.test.ts +123 -0
- package/src/cli/handlers/lifecycle-health.ts +157 -0
- package/src/cli/handlers/lifecycle.ts +1 -1
- package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
- package/src/cli/handlers/pr-record.test.ts +201 -0
- package/src/cli/handlers/pr.test.ts +22 -0
- package/src/cli/handlers/pr.ts +127 -4
- package/src/cli/handlers/promote.test.ts +17 -0
- package/src/cli/handlers/promote.ts +16 -1
- package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
- package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
- package/src/cli/handlers/release.e2e.test.ts +19 -28
- package/src/cli/handlers/release.ts +81 -11
- package/src/cli/main.ts +81 -21
- package/src/cli/parse-args.ts +17 -1
- package/src/cli/registry.ts +18 -4
- package/src/components/deploy-inputs.test.ts +108 -0
- package/src/components/deploy-inputs.ts +225 -0
- package/src/components/driver.ts +11 -0
- package/src/components/gate-plan.ts +9 -0
- package/src/components/pr-pipeline.ts +56 -15
- package/src/components/pr-run.ts +1 -1
- package/src/components/release-compile.test.ts +34 -0
- package/src/components/release-compile.ts +89 -8
- package/src/components/release-promote.test.ts +55 -2
- package/src/components/release-promote.ts +93 -23
- package/src/components/verbs/bom-writer.ts +1 -1
- package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
- package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
- package/src/components/wave-plan.ts +32 -4
- package/src/graph-detail.test.ts +57 -0
- package/src/graph-detail.ts +33 -7
- package/src/health.schema.json +191 -0
- package/src/health.test.ts +62 -0
- package/src/health.ts +178 -0
- package/src/lexicon.ts +41 -1
- package/src/lifecycle/merge-record.test.ts +111 -0
- package/src/lifecycle/merge-record.ts +154 -0
- package/src/lifecycle/publish-attachments.ts +25 -13
- package/src/lifecycle/publish-npm.ts +4 -3
- package/src/lifecycle/publish-targets.test.ts +34 -4
- package/src/lifecycle/release-at.test.ts +48 -0
- package/src/lifecycle/release-at.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +41 -0
- package/src/lifecycle/release-cut.ts +22 -11
- package/src/lifecycle/release-publish.test.ts +43 -13
- package/src/lifecycle/release-publish.ts +38 -18
- package/src/lifecycle/release-store.ts +57 -13
- package/src/lifecycle/run-ledger.test.ts +13 -0
- package/src/lifecycle/run-ledger.ts +1 -0
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
- package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
- package/src/op/activities/activity-contracts.ts +3 -0
- package/src/op/activities/shell-container.e2e.test.ts +67 -0
- package/src/op/activities/shell.test.ts +44 -1
- package/src/op/activities/shell.ts +110 -3
- package/src/op/builders.ts +7 -0
- package/src/op/runtime.ts +2 -0
- package/src/op/step-console.test.ts +4 -2
- package/src/pr-loop.test.ts +29 -0
- package/src/pr-loop.ts +26 -7
- package/src/pr-report-url.ts +2 -2
- package/src/runner-start-bundle.test.ts +29 -0
- package/src/runner-start.test.ts +179 -0
- package/src/runner-start.ts +329 -0
- package/src/workspace/check-contract.test.ts +3 -2
- package/src/workspace/check.schema.json +70 -2
- package/src/workspace/checks/security.test.ts +138 -0
- package/src/workspace/checks/security.ts +305 -0
- package/src/workspace/checks.ts +5 -0
- package/src/workspace/ci-builds.e2e.test.ts +208 -0
- package/src/workspace/ci-builds.test.ts +174 -0
- package/src/workspace/ci-builds.ts +385 -0
- package/src/workspace/ci-green-builds.test.ts +180 -0
- package/src/workspace/ci-green-cli.test.ts +54 -0
- package/src/workspace/ci-green-cli.ts +26 -5
- package/src/workspace/ci-green-forge.ts +24 -0
- package/src/workspace/ci-green-workflow.test.ts +84 -1
- package/src/workspace/ci-green-workflow.ts +139 -2
- package/src/workspace/ci-green.test.ts +5 -2
- package/src/workspace/ci-green.ts +359 -27
- package/src/workspace/ci-last-green.schema.json +49 -0
- package/src/workspace/ci-list.schema.json +35 -1
- package/src/workspace/ci-run-index.schema.json +1 -1
- package/src/workspace/ci-run-index.ts +1 -1
- package/src/workspace/ci-run.test.ts +24 -7
- package/src/workspace/ci-run.ts +78 -16
- package/src/workspace/ci-runner-start.test.ts +151 -0
- package/src/workspace/ci-runner-start.ts +193 -0
- package/src/workspace/ci-serve-builds.test.ts +86 -0
- package/src/workspace/ci-serve.test.ts +59 -0
- package/src/workspace/ci-serve.ts +177 -22
- package/src/workspace/declaration.schema.json +23 -0
- package/src/workspace/declaration.test.ts +17 -0
- package/src/workspace/declaration.ts +22 -1
- package/src/workspace/ls.schema.json +2 -1
- package/src/workspace/ls.ts +3 -2
- package/src/workspace/member-pipeline.ts +2 -1
- package/src/workspace/merge-record.schema.json +49 -0
- package/src/workspace/pr-report.schema.json +3 -3
- package/src/workspace/reason-codes.test.ts +2 -0
- package/src/workspace/reason-codes.ts +18 -0
- package/src/workspace/release-candidates.schema.json +353 -0
- package/src/workspace/release-candidates.test.ts +106 -0
- package/src/workspace/release-candidates.ts +315 -0
- package/src/workspace/security.test.ts +70 -0
- package/src/workspace/security.ts +118 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/health/v1/health.schema.json",
|
|
4
|
+
"title": "chant-health/v1",
|
|
5
|
+
"description": "What `chant lifecycle health <env> --json` prints (#4177): whether each declared resource that has a health read is up, read separately from drift. Every entity a read applies to lands in exactly one of `entities` (read, with a state), `unobserved` (the read failed, never folded into unhealthy) or `absent` (the provider says it is not deployed). Entity names are the graph's node ids. A single-stack project puts `lexicons` at the top level; a multi-stack one nests them under `stacks`. Written by the first chant release after 0.124.32. Readers ignore fields they do not know; a field is only ever added within a version.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": [
|
|
8
|
+
"schema",
|
|
9
|
+
"chantVersion",
|
|
10
|
+
"environment",
|
|
11
|
+
"observedAt",
|
|
12
|
+
"summary"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"schema": {
|
|
16
|
+
"const": "chant-health/v1"
|
|
17
|
+
},
|
|
18
|
+
"chantVersion": {
|
|
19
|
+
"type": "string",
|
|
20
|
+
"description": "The chant that ran the read."
|
|
21
|
+
},
|
|
22
|
+
"environment": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "The environment named on the command line."
|
|
25
|
+
},
|
|
26
|
+
"observedAt": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "When the command started reading, ISO 8601 UTC."
|
|
29
|
+
},
|
|
30
|
+
"summary": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"required": [
|
|
33
|
+
"healthy",
|
|
34
|
+
"unhealthy",
|
|
35
|
+
"unknown",
|
|
36
|
+
"unobserved",
|
|
37
|
+
"absent"
|
|
38
|
+
],
|
|
39
|
+
"description": "Counts over every lexicon and stack.",
|
|
40
|
+
"properties": {
|
|
41
|
+
"healthy": {
|
|
42
|
+
"type": "integer",
|
|
43
|
+
"minimum": 0
|
|
44
|
+
},
|
|
45
|
+
"unhealthy": {
|
|
46
|
+
"type": "integer",
|
|
47
|
+
"minimum": 0
|
|
48
|
+
},
|
|
49
|
+
"unknown": {
|
|
50
|
+
"type": "integer",
|
|
51
|
+
"minimum": 0
|
|
52
|
+
},
|
|
53
|
+
"unobserved": {
|
|
54
|
+
"type": "integer",
|
|
55
|
+
"minimum": 0
|
|
56
|
+
},
|
|
57
|
+
"absent": {
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 0
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"oneOf": [
|
|
65
|
+
{
|
|
66
|
+
"required": [
|
|
67
|
+
"lexicons"
|
|
68
|
+
],
|
|
69
|
+
"properties": {
|
|
70
|
+
"lexicons": {
|
|
71
|
+
"$ref": "#/$defs/byLexicon"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"required": [
|
|
77
|
+
"stacks"
|
|
78
|
+
],
|
|
79
|
+
"properties": {
|
|
80
|
+
"stacks": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"description": "Per stack name, the same per-lexicon results.",
|
|
83
|
+
"additionalProperties": {
|
|
84
|
+
"$ref": "#/$defs/byLexicon"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"$defs": {
|
|
91
|
+
"byLexicon": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"description": "Per lexicon name, that lexicon's read. A lexicon with no applicable entity is left out.",
|
|
94
|
+
"additionalProperties": {
|
|
95
|
+
"$ref": "#/$defs/read"
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"read": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"required": [
|
|
101
|
+
"entities",
|
|
102
|
+
"unobserved",
|
|
103
|
+
"absent"
|
|
104
|
+
],
|
|
105
|
+
"properties": {
|
|
106
|
+
"entities": {
|
|
107
|
+
"type": "object",
|
|
108
|
+
"additionalProperties": {
|
|
109
|
+
"$ref": "#/$defs/entry"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"unobserved": {
|
|
113
|
+
"type": "object",
|
|
114
|
+
"additionalProperties": {
|
|
115
|
+
"$ref": "#/$defs/hole"
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"absent": {
|
|
119
|
+
"type": "array",
|
|
120
|
+
"items": {
|
|
121
|
+
"type": "string"
|
|
122
|
+
},
|
|
123
|
+
"description": "Entity names the provider says are not deployed."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"entry": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"required": [
|
|
130
|
+
"type",
|
|
131
|
+
"state",
|
|
132
|
+
"detail",
|
|
133
|
+
"observedAt"
|
|
134
|
+
],
|
|
135
|
+
"properties": {
|
|
136
|
+
"type": {
|
|
137
|
+
"type": "string",
|
|
138
|
+
"description": "Entity type, such as AWS::ECS::Service."
|
|
139
|
+
},
|
|
140
|
+
"state": {
|
|
141
|
+
"enum": [
|
|
142
|
+
"healthy",
|
|
143
|
+
"unhealthy",
|
|
144
|
+
"unknown"
|
|
145
|
+
],
|
|
146
|
+
"description": "healthy and unhealthy are what the provider reported. unknown means the read succeeded and the answer decides neither way: a rollout in progress, a service scaled to zero, a condition pending."
|
|
147
|
+
},
|
|
148
|
+
"detail": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "One short line saying why, in the provider's terms."
|
|
151
|
+
},
|
|
152
|
+
"observedAt": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "When chant read it, ISO 8601."
|
|
155
|
+
},
|
|
156
|
+
"checkedAt": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "When the provider last checked or changed the state, when it says, ISO 8601."
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"hole": {
|
|
163
|
+
"type": "object",
|
|
164
|
+
"required": [
|
|
165
|
+
"reason"
|
|
166
|
+
],
|
|
167
|
+
"description": "A read that failed. Never counted as unhealthy.",
|
|
168
|
+
"properties": {
|
|
169
|
+
"type": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"reason": {
|
|
173
|
+
"enum": [
|
|
174
|
+
"read-failed",
|
|
175
|
+
"no-credentials",
|
|
176
|
+
"no-binding",
|
|
177
|
+
"unsupported-kind",
|
|
178
|
+
"filtered"
|
|
179
|
+
]
|
|
180
|
+
},
|
|
181
|
+
"detail": {
|
|
182
|
+
"type": "string"
|
|
183
|
+
},
|
|
184
|
+
"queried": {
|
|
185
|
+
"type": "string",
|
|
186
|
+
"description": "The address the read was issued against, when the lexicon reports it."
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { healthApplicable, observeHealthFor, summarizeHealth, totalHealth, type HealthLexicon } from "./health";
|
|
3
|
+
|
|
4
|
+
const entities = new Map([
|
|
5
|
+
["web", { entityType: "AWS::ECS::Service", props: {} }],
|
|
6
|
+
["check", { entityType: "AWS::Route53::HealthCheck", props: {} }],
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
describe("health reads (#4177)", () => {
|
|
10
|
+
test("healthApplicable keeps only the types the lexicon names", () => {
|
|
11
|
+
const all = new Map([...entities, ["bucket", { entityType: "AWS::S3::Bucket", props: {} }]]);
|
|
12
|
+
const picked = healthApplicable({ name: "aws", healthTypes: ["AWS::ECS::Service"] }, all);
|
|
13
|
+
expect([...picked.keys()]).toEqual(["web"]);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("a thrown reader is read-failed for every entity, never unhealthy", async () => {
|
|
17
|
+
const plugin: HealthLexicon = {
|
|
18
|
+
name: "aws",
|
|
19
|
+
healthTypes: ["AWS::ECS::Service"],
|
|
20
|
+
observeHealth: async () => {
|
|
21
|
+
throw new Error("connect ECONNREFUSED");
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
const r = await observeHealthFor(plugin, { environment: "dev", buildOutput: "", entities });
|
|
25
|
+
expect(r.entities).toEqual({});
|
|
26
|
+
expect(r.unobserved.web).toEqual({ reason: "read-failed", type: "AWS::ECS::Service", detail: "connect ECONNREFUSED" });
|
|
27
|
+
expect(Object.keys(r.unobserved)).toEqual(["web", "check"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a lexicon without a reader reports unsupported-kind", async () => {
|
|
31
|
+
const r = await observeHealthFor({ name: "aws" }, { environment: "dev", buildOutput: "", entities });
|
|
32
|
+
expect(r.unobserved.web?.reason).toBe("unsupported-kind");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the result is made total, and entities win over absent and unobserved", () => {
|
|
36
|
+
const r = totalHealth(["web", "check", "gone", "forgot"], {
|
|
37
|
+
entities: { web: { type: "AWS::ECS::Service", state: "healthy", detail: "1/1 running", observedAt: "t" } },
|
|
38
|
+
unobserved: { web: { reason: "read-failed" }, check: { reason: "no-credentials" } },
|
|
39
|
+
absent: ["gone", "web"],
|
|
40
|
+
});
|
|
41
|
+
expect(Object.keys(r.entities)).toEqual(["web"]);
|
|
42
|
+
expect(r.absent).toEqual(["gone"]);
|
|
43
|
+
expect(r.unobserved.check?.reason).toBe("no-credentials");
|
|
44
|
+
expect(r.unobserved.forgot?.reason).toBe("read-failed");
|
|
45
|
+
expect(r.unobserved.web).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("summarizeHealth counts each place", () => {
|
|
49
|
+
const s = summarizeHealth([
|
|
50
|
+
{
|
|
51
|
+
entities: {
|
|
52
|
+
a: { type: "t", state: "healthy", detail: "", observedAt: "" },
|
|
53
|
+
b: { type: "t", state: "unhealthy", detail: "", observedAt: "" },
|
|
54
|
+
c: { type: "t", state: "unknown", detail: "", observedAt: "" },
|
|
55
|
+
},
|
|
56
|
+
unobserved: { d: { reason: "read-failed" } },
|
|
57
|
+
absent: ["e", "f"],
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
expect(s).toEqual({ healthy: 1, unhealthy: 1, unknown: 1, unobserved: 1, absent: 2 });
|
|
61
|
+
});
|
|
62
|
+
});
|
package/src/health.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime health (#4177): whether a declared thing is up, read separately from
|
|
3
|
+
* drift.
|
|
4
|
+
*
|
|
5
|
+
* The observation family answers what is deployed (`describeResources`), what
|
|
6
|
+
* its properties are (`observeResourcesDeep`) and what it would do
|
|
7
|
+
* (`predictBehaviour`). None of them says whether a service is serving. A
|
|
8
|
+
* lexicon that can read that implements {@link HealthLexicon.observeHealth}
|
|
9
|
+
* for the types it names in `healthTypes`, and `chant lifecycle health` reports
|
|
10
|
+
* the answers in the {@link HEALTH_SCHEMA} shape.
|
|
11
|
+
*
|
|
12
|
+
* The verdicts follow #1089's rule. Every entity the caller asks about lands in
|
|
13
|
+
* exactly one place: `entities` (read, with a state), `unobserved` (the read
|
|
14
|
+
* failed, a hole with a reason) or `absent` (the provider says it is not
|
|
15
|
+
* deployed). A failed read is never `unhealthy`: `unhealthy` is something the
|
|
16
|
+
* provider said.
|
|
17
|
+
*
|
|
18
|
+
* Routing is not modelled. A consumer that wants rotation (a latency record
|
|
19
|
+
* whose health check is unhealthy is out of rotation) derives it from the
|
|
20
|
+
* graph's edges and these states.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { UnobservedEntity, UnobservedReason } from "./observation";
|
|
24
|
+
import { unobservedAll } from "./observation";
|
|
25
|
+
|
|
26
|
+
/** The versioned shape `chant lifecycle health --json` prints. The JSON Schema is `./health.schema.json`. */
|
|
27
|
+
export const HEALTH_SCHEMA = "chant-health/v1";
|
|
28
|
+
|
|
29
|
+
/** The `$id` of `./health.schema.json`, which the package exports as `@intentius/chant/health.schema.json`. */
|
|
30
|
+
export const HEALTH_SCHEMA_ID = "https://intentius.io/chant/schemas/health/v1/health.schema.json";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `healthy` and `unhealthy` are what the provider reported. `unknown` means the
|
|
34
|
+
* read succeeded and the provider's answer does not decide either way (a
|
|
35
|
+
* deployment still rolling, a service scaled to zero, a condition pending).
|
|
36
|
+
*/
|
|
37
|
+
export type HealthState = "healthy" | "unhealthy" | "unknown";
|
|
38
|
+
|
|
39
|
+
export interface HealthEntry {
|
|
40
|
+
/** Entity type, e.g. `AWS::ECS::Service`. */
|
|
41
|
+
type: string;
|
|
42
|
+
state: HealthState;
|
|
43
|
+
/** One short line saying why, in the provider's terms. */
|
|
44
|
+
detail: string;
|
|
45
|
+
/** When chant read it (ISO 8601). */
|
|
46
|
+
observedAt: string;
|
|
47
|
+
/** When the provider last checked or changed the state, when it says (ISO 8601). */
|
|
48
|
+
checkedAt?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** What one lexicon's health read returns. */
|
|
52
|
+
export interface HealthReadResult {
|
|
53
|
+
/** Read, keyed by chant entity name, which is the graph's node id. */
|
|
54
|
+
entities: Record<string, HealthEntry>;
|
|
55
|
+
/** Could not be read. Never folded into `unhealthy`. */
|
|
56
|
+
unobserved: Record<string, UnobservedEntity>;
|
|
57
|
+
/** The provider says these are not deployed. */
|
|
58
|
+
absent: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The options core passes a lexicon's health read. They mirror `observeResourcesDeep`'s. */
|
|
62
|
+
export interface HealthReadOptions {
|
|
63
|
+
environment: string;
|
|
64
|
+
buildOutput: string;
|
|
65
|
+
/** Only entities whose type is in the lexicon's `healthTypes`. */
|
|
66
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
67
|
+
stack?: string;
|
|
68
|
+
region?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The two plugin members a lexicon implements to answer a health read. */
|
|
72
|
+
export interface HealthLexicon {
|
|
73
|
+
name: string;
|
|
74
|
+
/** Entity types {@link observeHealth} can read. Data, so core picks the entities without calling the reader. */
|
|
75
|
+
healthTypes?: readonly string[];
|
|
76
|
+
observeHealth?(options: HealthReadOptions): Promise<HealthReadResult>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The entities of one lexicon a health read applies to: those whose type the
|
|
81
|
+
* lexicon names in `healthTypes`.
|
|
82
|
+
*/
|
|
83
|
+
export function healthApplicable(
|
|
84
|
+
plugin: HealthLexicon,
|
|
85
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>,
|
|
86
|
+
): Map<string, { entityType: string; props: Record<string, unknown> }> {
|
|
87
|
+
const types = new Set(plugin.healthTypes ?? []);
|
|
88
|
+
const out = new Map<string, { entityType: string; props: Record<string, unknown> }>();
|
|
89
|
+
for (const [name, entity] of entities) if (types.has(entity.entityType)) out.set(name, entity);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run one lexicon's health read. Never throws: a thrown reader reports every
|
|
95
|
+
* applicable entity `read-failed`. The result is made total, so an entity the
|
|
96
|
+
* reader forgot comes back `unobserved` rather than missing.
|
|
97
|
+
*/
|
|
98
|
+
export async function observeHealthFor(
|
|
99
|
+
plugin: HealthLexicon,
|
|
100
|
+
options: HealthReadOptions,
|
|
101
|
+
): Promise<HealthReadResult> {
|
|
102
|
+
const names = [...options.entities.keys()];
|
|
103
|
+
if (names.length === 0) return { entities: {}, unobserved: {}, absent: [] };
|
|
104
|
+
if (!plugin.observeHealth) {
|
|
105
|
+
return {
|
|
106
|
+
entities: {},
|
|
107
|
+
unobserved: unobservedAll(names, "unsupported-kind", `${plugin.name} has no health read`, options.entities),
|
|
108
|
+
absent: [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
let result: HealthReadResult;
|
|
112
|
+
try {
|
|
113
|
+
result = await plugin.observeHealth(options);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
116
|
+
return { entities: {}, unobserved: unobservedAll(names, "read-failed", message, options.entities), absent: [] };
|
|
117
|
+
}
|
|
118
|
+
return totalHealth(names, result, options.entities);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Put every asked-about name in exactly one of the three places. A name in
|
|
123
|
+
* `entities` wins over the others, then `absent`, then `unobserved`; a name in
|
|
124
|
+
* none of them becomes `read-failed`.
|
|
125
|
+
*/
|
|
126
|
+
export function totalHealth(
|
|
127
|
+
names: string[],
|
|
128
|
+
result: HealthReadResult,
|
|
129
|
+
types?: Map<string, { entityType: string }>,
|
|
130
|
+
): HealthReadResult {
|
|
131
|
+
const entities: Record<string, HealthEntry> = {};
|
|
132
|
+
const unobserved: Record<string, UnobservedEntity> = {};
|
|
133
|
+
const absentSet = new Set(result.absent ?? []);
|
|
134
|
+
const absent: string[] = [];
|
|
135
|
+
for (const name of names) {
|
|
136
|
+
const entry = result.entities?.[name];
|
|
137
|
+
if (entry) entities[name] = entry;
|
|
138
|
+
else if (absentSet.has(name)) absent.push(name);
|
|
139
|
+
else if (result.unobserved?.[name]) unobserved[name] = result.unobserved[name];
|
|
140
|
+
else Object.assign(unobserved, unobservedAll([name], "read-failed", "the health read returned no answer for this entity", types));
|
|
141
|
+
}
|
|
142
|
+
return { entities, unobserved, absent };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Counts across a health read, for the summary line and `--json`. */
|
|
146
|
+
export interface HealthSummary {
|
|
147
|
+
healthy: number;
|
|
148
|
+
unhealthy: number;
|
|
149
|
+
unknown: number;
|
|
150
|
+
unobserved: number;
|
|
151
|
+
absent: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function summarizeHealth(results: Iterable<HealthReadResult>): HealthSummary {
|
|
155
|
+
const summary: HealthSummary = { healthy: 0, unhealthy: 0, unknown: 0, unobserved: 0, absent: 0 };
|
|
156
|
+
for (const r of results) {
|
|
157
|
+
for (const entry of Object.values(r.entities)) summary[entry.state]++;
|
|
158
|
+
summary.unobserved += Object.keys(r.unobserved).length;
|
|
159
|
+
summary.absent += r.absent.length;
|
|
160
|
+
}
|
|
161
|
+
return summary;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A health entry, for a reader building its result. */
|
|
165
|
+
export function healthEntry(
|
|
166
|
+
type: string,
|
|
167
|
+
state: HealthState,
|
|
168
|
+
detail: string,
|
|
169
|
+
observedAt: string,
|
|
170
|
+
checkedAt?: string,
|
|
171
|
+
): HealthEntry {
|
|
172
|
+
return { type, state, detail, observedAt, ...(checkedAt ? { checkedAt } : {}) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A hole, for a reader building its result. */
|
|
176
|
+
export function healthHole(type: string, reason: UnobservedReason, detail: string): UnobservedEntity {
|
|
177
|
+
return { type, reason, detail };
|
|
178
|
+
}
|
package/src/lexicon.ts
CHANGED
|
@@ -78,6 +78,15 @@ export type {
|
|
|
78
78
|
UnobservedReason,
|
|
79
79
|
} from "./observation";
|
|
80
80
|
|
|
81
|
+
// The runtime-health contract (#4177), re-exported so a lexicon's
|
|
82
|
+
// `observeHealth` types itself from the same entry.
|
|
83
|
+
export type {
|
|
84
|
+
HealthEntry,
|
|
85
|
+
HealthReadOptions,
|
|
86
|
+
HealthReadResult,
|
|
87
|
+
HealthState,
|
|
88
|
+
} from "./health";
|
|
89
|
+
|
|
81
90
|
// The identity contract (#1982), re-exported from the same entry so a
|
|
82
91
|
// lexicon's `describeIdentity` types itself without a second import path.
|
|
83
92
|
// Runtime helpers live in `@intentius/chant/identity`.
|
|
@@ -691,7 +700,19 @@ export type OpFindingMode = "report" | "issue" | "comment" | "pull-request" | "m
|
|
|
691
700
|
export type OpTrigger =
|
|
692
701
|
| { kind: "cron"; schedule: string }
|
|
693
702
|
| { kind: "pull_request"; branches?: string[] }
|
|
694
|
-
| {
|
|
703
|
+
| {
|
|
704
|
+
kind: "push";
|
|
705
|
+
branches?: string[];
|
|
706
|
+
/**
|
|
707
|
+
* Which pushes run the Op (#4169), for an Op a `ci.green` phase names
|
|
708
|
+
* with `build` and the tick reads from chant runs. `every`: each
|
|
709
|
+
* commit, in a run of its own that no later push cancels. `newest`:
|
|
710
|
+
* one run at a time, the newest push waiting and the ones between
|
|
711
|
+
* dropped, plus a run at each `ci/build/<sha>` tag the tick or `ci run
|
|
712
|
+
* --at` pushes. Absent: one run at a time per Op, as before.
|
|
713
|
+
*/
|
|
714
|
+
build?: "every" | "newest";
|
|
715
|
+
};
|
|
695
716
|
|
|
696
717
|
/**
|
|
697
718
|
* One step a generated Op job runs between the checkout and the
|
|
@@ -1799,6 +1820,25 @@ export interface LexiconPlugin {
|
|
|
1799
1820
|
owned?: boolean;
|
|
1800
1821
|
}): Promise<DeepObservationResult>;
|
|
1801
1822
|
|
|
1823
|
+
/**
|
|
1824
|
+
* Entity types {@link observeHealth} reads (#4177). Data, not a method, so
|
|
1825
|
+
* core picks the entities a health read applies to without calling it.
|
|
1826
|
+
*/
|
|
1827
|
+
healthTypes?: readonly string[];
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Whether each declared entity of a {@link healthTypes} type is up (#4177),
|
|
1831
|
+
* read separately from drift: a service's running count, a health check's
|
|
1832
|
+
* checkers, a revision's running state, a Ready condition.
|
|
1833
|
+
*
|
|
1834
|
+
* Every entity in `options.entities` lands in exactly one of `entities`,
|
|
1835
|
+
* `unobserved` or `absent`. A read that failed is `unobserved` with a reason,
|
|
1836
|
+
* never `unhealthy`: `unhealthy` is something the provider said. Throwing is
|
|
1837
|
+
* the whole-lexicon failure, and core reports every entity `read-failed`.
|
|
1838
|
+
* Endpoint overrides apply as on the other reads, so an emulator answers.
|
|
1839
|
+
*/
|
|
1840
|
+
observeHealth?(options: import("./health").HealthReadOptions): Promise<import("./health").HealthReadResult>;
|
|
1841
|
+
|
|
1802
1842
|
/**
|
|
1803
1843
|
* This lexicon's noise rules for deep observation (#1014): which fields are
|
|
1804
1844
|
* read-only / server-populated / controller-managed / provider-defaulted, and
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge records (#4168 step 1) against temp git repos: written under the
|
|
3
|
+
* commit on chant/lifecycle, rewritten only when the record changed, read
|
|
4
|
+
* back, and validated against the schema; and where the change came from.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { MERGE_RECORD_CONTRACT, MERGE_RECORD_SCHEMA_ID, MERGES_DIR, mergeRecordFile, mergeSourceOf, readMergeRecord, writeMergeRecord, type MergeRecord } from "./merge-record";
|
|
13
|
+
import { contract, validSchema } from "../workspace/__fixtures__/contract-repo";
|
|
14
|
+
import schema from "../workspace/merge-record.schema.json";
|
|
15
|
+
|
|
16
|
+
const { expectValid } = contract(schema);
|
|
17
|
+
|
|
18
|
+
function git(dir: string, ...args: string[]): string {
|
|
19
|
+
const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
|
|
20
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
21
|
+
return r.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function initRepo(dir: string): string {
|
|
25
|
+
git(dir, "init", "-q", "-b", "main");
|
|
26
|
+
git(dir, "config", "user.email", "test@chant.dev");
|
|
27
|
+
git(dir, "config", "user.name", "Test");
|
|
28
|
+
writeFileSync(join(dir, "README.md"), "fixture\n");
|
|
29
|
+
git(dir, "add", ".");
|
|
30
|
+
git(dir, "commit", "-q", "-m", "init");
|
|
31
|
+
return git(dir, "rev-parse", "HEAD");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const record = (commit: string, over: Partial<MergeRecord> = {}): MergeRecord => ({
|
|
35
|
+
$schema: MERGE_RECORD_SCHEMA_ID,
|
|
36
|
+
contract: MERGE_RECORD_CONTRACT,
|
|
37
|
+
commit,
|
|
38
|
+
base: "b".repeat(40),
|
|
39
|
+
env: "prod",
|
|
40
|
+
source: { kind: "pull-request", pr: 12 },
|
|
41
|
+
op: "pr-12",
|
|
42
|
+
gate: "pr-apply",
|
|
43
|
+
digest: `jcs1-sha256:${"1".repeat(64)}`,
|
|
44
|
+
plan: "approved",
|
|
45
|
+
approvedBy: ["github:rev"],
|
|
46
|
+
components: ["net", "app"],
|
|
47
|
+
recordedAt: "2026-10-20T00:00:00.000Z",
|
|
48
|
+
...over,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("the merge record schema", () => {
|
|
52
|
+
test("is a valid schema and names this module's id", () => {
|
|
53
|
+
expect(validSchema(schema)).toBe(true);
|
|
54
|
+
expect(schema.$id).toBe(MERGE_RECORD_SCHEMA_ID);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("each source and plan status validates", () => {
|
|
58
|
+
const sha = "a".repeat(40);
|
|
59
|
+
expectValid(record(sha));
|
|
60
|
+
expectValid(record(sha, { source: { kind: "work-item", records: [{ kind: "work", id: "w-7" }] }, op: null, plan: "unapproved", approvedBy: [] }));
|
|
61
|
+
expectValid(record(sha, { source: { kind: "direct-push" }, op: null, digest: null, plan: "nothing", approvedBy: [], components: [] }));
|
|
62
|
+
expectValid(record(sha, { plan: "changed", approvedDigest: `jcs1-sha256:${"9".repeat(64)}`, approvedBy: [], authorApproved: ["github:carol"], member: "net" }));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("writeMergeRecord and readMergeRecord", () => {
|
|
67
|
+
test("writes the record under its commit, leaves the same record alone, and replaces a changed one", async () => {
|
|
68
|
+
await withTestDir(async (dir) => {
|
|
69
|
+
const sha = initRepo(dir);
|
|
70
|
+
expect(await readMergeRecord(sha, { cwd: dir })).toBeNull();
|
|
71
|
+
expect((await writeMergeRecord(record(sha), { cwd: dir })).written).toBe(true);
|
|
72
|
+
expect(git(dir, "ls-tree", "--name-only", `chant/lifecycle:${MERGES_DIR}`)).toBe(mergeRecordFile(sha));
|
|
73
|
+
expect(await readMergeRecord(sha, { cwd: dir })).toEqual(record(sha));
|
|
74
|
+
|
|
75
|
+
// The record job ran again: only the time differs, so nothing is written.
|
|
76
|
+
const tip = git(dir, "rev-parse", "chant/lifecycle");
|
|
77
|
+
expect((await writeMergeRecord(record(sha, { recordedAt: "2026-10-21T00:00:00.000Z" }), { cwd: dir })).written).toBe(false);
|
|
78
|
+
expect(git(dir, "rev-parse", "chant/lifecycle")).toBe(tip);
|
|
79
|
+
|
|
80
|
+
// A plan that moved since is a new record for the same commit.
|
|
81
|
+
const moved = record(sha, { plan: "changed", approvedDigest: `jcs1-sha256:${"9".repeat(64)}`, approvedBy: [] });
|
|
82
|
+
expect((await writeMergeRecord(moved, { cwd: dir })).written).toBe(true);
|
|
83
|
+
expect(await readMergeRecord(sha, { cwd: dir })).toEqual(moved);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a member's record sits under its own prefix", async () => {
|
|
88
|
+
await withTestDir(async (dir) => {
|
|
89
|
+
const sha = initRepo(dir);
|
|
90
|
+
await writeMergeRecord(record(sha, { member: "net" }), { cwd: dir, member: "net" });
|
|
91
|
+
expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle")).toBe(`_members/net/${MERGES_DIR}/${mergeRecordFile(sha)}`);
|
|
92
|
+
expect(await readMergeRecord(sha, { cwd: dir, member: "net" })).toMatchObject({ member: "net" });
|
|
93
|
+
expect(await readMergeRecord(sha, { cwd: dir })).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("mergeSourceOf", () => {
|
|
99
|
+
test("a pull request first, then the work items a commit's trailers name, then a direct push", async () => {
|
|
100
|
+
await withTestDir(async (dir) => {
|
|
101
|
+
const plain = initRepo(dir);
|
|
102
|
+
expect(mergeSourceOf(dir, plain, 12)).toEqual({ kind: "pull-request", pr: 12 });
|
|
103
|
+
expect(mergeSourceOf(dir, plain, null)).toEqual({ kind: "direct-push" });
|
|
104
|
+
writeFileSync(join(dir, "a.txt"), "a\n");
|
|
105
|
+
git(dir, "add", ".");
|
|
106
|
+
git(dir, "commit", "-q", "-m", "carry out w-7\n\nChant-Record: work:w-7\nChant-Record: decision:d-1\nChant-Record: work:w-8\nChant-Record: work:w-7");
|
|
107
|
+
expect(mergeSourceOf(dir, git(dir, "rev-parse", "HEAD"), null)).toEqual({ kind: "work-item", records: [{ kind: "work", id: "w-7" }, { kind: "work", id: "w-8" }] });
|
|
108
|
+
expect(mergeSourceOf(dir, "f".repeat(40), null)).toEqual({ kind: "direct-push" });
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|