@reventlessdev/reventless-spec 3.0.0-alpha.154 → 3.0.0-alpha.156
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 +23 -0
- package/package.json +1 -1
- package/src/components/Aggregate.res +4 -1
- package/src/components/InboundTranslationSlice.res +4 -1
- package/src/components/Owner.res +30 -0
- package/src/components/Owner.res.mjs +36 -0
- package/src/components/Plugin.res +8 -0
- package/src/components/Plugin.res.mjs +5 -2
- package/src/components/ReadModel.res +4 -1
- package/src/components/StateChangeSlice.res +4 -1
- package/src/components/StateViewSlice.res +4 -1
- package/src/lifecycle/CheckLifecycleModel.res +3 -0
- package/src/lifecycle/CheckLifecycleModel.res.mjs +3 -0
- package/src/types/AccountsManifest.res +23 -1
- package/src/types/AccountsManifest.res.mjs +18 -0
- package/src/types/Authorization.res +37 -10
- package/src/types/Authorization.res.mjs +29 -15
- package/src/types/DeploymentProvenance.res +84 -0
- package/src/types/DeploymentProvenance.res.mjs +86 -0
- package/src/types/Environments.res +230 -0
- package/src/types/Environments.res.mjs +314 -0
- package/src/types/Message.res +21 -0
- package/src/types/Message.res.mjs +16 -1
- package/src/types/OwnerScope.res +113 -6
- package/src/types/OwnerScope.res.mjs +110 -3
- package/src/types/Role.res +143 -0
- package/src/types/Role.res.mjs +129 -0
- package/src/types/RoleCoverage.res +84 -0
- package/src/types/RoleCoverage.res.mjs +72 -0
- package/src/types/authorizationNamed.mjs +17 -0
- package/src/types/AdminGroup.res +0 -27
- package/src/types/AdminGroup.res.mjs +0 -9
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/***
|
|
2
|
+
The environments an app deploys to, as a file: `environments.yaml`, beside its
|
|
3
|
+
`deploy-manifest.yaml`.
|
|
4
|
+
|
|
5
|
+
```yaml
|
|
6
|
+
environments:
|
|
7
|
+
- { name: review, purpose: review, pullRequestsInto: dev }
|
|
8
|
+
- { name: dev, purpose: dev, branch: dev }
|
|
9
|
+
- { name: test, purpose: test, branch: test }
|
|
10
|
+
- { name: production, purpose: production, branch: main, tags: "v*", stack: prod }
|
|
11
|
+
- { name: demo, purpose: demo, tags: "v*" }
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A *shared* environment has one stack and is fed by a branch, by tags, or by both.
|
|
15
|
+
A *review* environment is fed by the pull requests into one branch and has one
|
|
16
|
+
stack per open pull request, named `<name>-pr-<number>`.
|
|
17
|
+
|
|
18
|
+
Here rather than in the AWS package because more than the deploy reads it: a
|
|
19
|
+
tool that shows which commit runs where needs the same answer to *which branch
|
|
20
|
+
feeds which environment* as the workflow that deploys, and a second reading of
|
|
21
|
+
the file is how the two would come to disagree.
|
|
22
|
+
|
|
23
|
+
The file is optional. Without it a deploy uses the branch name as the stack, as
|
|
24
|
+
it always has; nothing here is consulted.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
What an environment is for. `Other` keeps a team's own word rather than refusing
|
|
29
|
+
the file, because a purpose nothing here branches on costs nothing to carry.
|
|
30
|
+
*/
|
|
31
|
+
type purpose =
|
|
32
|
+
| Dev
|
|
33
|
+
| Test
|
|
34
|
+
| Production
|
|
35
|
+
| Demo
|
|
36
|
+
| Review
|
|
37
|
+
| Other(string)
|
|
38
|
+
|
|
39
|
+
let purposeSchema: S.t<purpose> = S.union([
|
|
40
|
+
S.literal("dev")->S.shape(_ => Dev),
|
|
41
|
+
S.literal("test")->S.shape(_ => Test),
|
|
42
|
+
S.literal("production")->S.shape(_ => Production),
|
|
43
|
+
S.literal("demo")->S.shape(_ => Demo),
|
|
44
|
+
S.literal("review")->S.shape(_ => Review),
|
|
45
|
+
S.string->S.shape(other => Other(other)),
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
let purposeName = (purpose: purpose): string =>
|
|
49
|
+
switch purpose {
|
|
50
|
+
| Dev => "dev"
|
|
51
|
+
| Test => "test"
|
|
52
|
+
| Production => "production"
|
|
53
|
+
| Demo => "demo"
|
|
54
|
+
| Review => "review"
|
|
55
|
+
| Other(name) => name
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One entry as the file spells it. [parseString] turns it into an [environment]. */
|
|
59
|
+
@schema
|
|
60
|
+
type entry = {
|
|
61
|
+
name: string,
|
|
62
|
+
purpose: @s.matches(purposeSchema) purpose,
|
|
63
|
+
branch?: string,
|
|
64
|
+
tags?: string,
|
|
65
|
+
stack?: string,
|
|
66
|
+
pullRequestsInto?: string,
|
|
67
|
+
drafts?: bool,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@schema
|
|
71
|
+
type file = {environments: array<entry>}
|
|
72
|
+
|
|
73
|
+
/** What feeds an environment, and so how many stacks it has. */
|
|
74
|
+
type feed =
|
|
75
|
+
| Shared({branch: option<string>, tags: option<string>, stack: string})
|
|
76
|
+
| ReviewOf({into: string, drafts: bool})
|
|
77
|
+
|
|
78
|
+
type environment = {name: string, purpose: purpose, feed: feed}
|
|
79
|
+
|
|
80
|
+
type t = array<environment>
|
|
81
|
+
|
|
82
|
+
// ── Reading ──────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
85
|
+
@module("yaml") external stringifyYaml: JSON.t => string = "stringify"
|
|
86
|
+
|
|
87
|
+
let environmentOf = (entry: entry): result<environment, string> =>
|
|
88
|
+
switch (entry.pullRequestsInto, entry.branch, entry.tags) {
|
|
89
|
+
| (Some(_), Some(_), _) | (Some(_), _, Some(_)) =>
|
|
90
|
+
Error(`${entry.name}: pullRequestsInto cannot be combined with branch or tags`)
|
|
91
|
+
| (Some(_), None, None) if entry.stack->Option.isSome =>
|
|
92
|
+
Error(`${entry.name}: a review environment names its stacks itself; remove stack`)
|
|
93
|
+
| (Some(into), None, None) =>
|
|
94
|
+
Ok({
|
|
95
|
+
name: entry.name,
|
|
96
|
+
purpose: entry.purpose,
|
|
97
|
+
feed: ReviewOf({into, drafts: entry.drafts->Option.getOr(false)}),
|
|
98
|
+
})
|
|
99
|
+
| (None, None, None) =>
|
|
100
|
+
Error(`${entry.name}: says neither branch, tags nor pullRequestsInto, so nothing feeds it`)
|
|
101
|
+
| (None, _, _) if entry.drafts->Option.isSome =>
|
|
102
|
+
Error(`${entry.name}: drafts is only meaningful with pullRequestsInto`)
|
|
103
|
+
| (None, branch, tags) =>
|
|
104
|
+
Ok({
|
|
105
|
+
name: entry.name,
|
|
106
|
+
purpose: entry.purpose,
|
|
107
|
+
feed: Shared({branch, tags, stack: entry.stack->Option.getOr(entry.name)}),
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Parses an environments document. Strict, like the accounts manifest: a
|
|
112
|
+
malformed entry refuses the whole file, because a skipped environment is a
|
|
113
|
+
branch that silently stops deploying. */
|
|
114
|
+
let parseString = (yamlText: string): result<t, string> =>
|
|
115
|
+
switch try Ok(S.parseOrThrow(parseYaml(yamlText), ~to=fileSchema)) catch {
|
|
116
|
+
| JsExn(err) => Error(JsExn.message(err)->Option.getOr("YAML parse error"))
|
|
117
|
+
| _ => Error("YAML parse error")
|
|
118
|
+
} {
|
|
119
|
+
| Error(_) as e => e
|
|
120
|
+
| Ok({environments}) =>
|
|
121
|
+
let names = environments->Array.map(e => e.name)
|
|
122
|
+
switch names->Array.find(n => names->Array.filter(m => m == n)->Array.length > 1) {
|
|
123
|
+
| Some(duplicate) => Error(`${duplicate}: named twice`)
|
|
124
|
+
| None =>
|
|
125
|
+
environments->Array.reduce(Ok([]), (acc, entry) =>
|
|
126
|
+
switch (acc, environmentOf(entry)) {
|
|
127
|
+
| (Error(_) as e, _) => e
|
|
128
|
+
| (Ok(_), Error(message)) => Error(message)
|
|
129
|
+
| (Ok(done), Ok(environment)) => Ok(done->Array.concat([environment]))
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let parseFile = (path: string): result<t, string> =>
|
|
136
|
+
try {
|
|
137
|
+
parseString(NodeFs.readFileSync(path))
|
|
138
|
+
} catch {
|
|
139
|
+
| JsExn(err) => Error(JsExn.message(err)->Option.getOr(`Cannot read ${path}`))
|
|
140
|
+
| _ => Error(`Cannot read ${path}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let entryOf = (environment: environment): entry =>
|
|
144
|
+
switch environment.feed {
|
|
145
|
+
| Shared({branch, tags, stack}) => {
|
|
146
|
+
name: environment.name,
|
|
147
|
+
purpose: environment.purpose,
|
|
148
|
+
?branch,
|
|
149
|
+
?tags,
|
|
150
|
+
stack: ?(stack == environment.name ? None : Some(stack)),
|
|
151
|
+
}
|
|
152
|
+
| ReviewOf({into, drafts}) => {
|
|
153
|
+
name: environment.name,
|
|
154
|
+
purpose: environment.purpose,
|
|
155
|
+
pullRequestsInto: into,
|
|
156
|
+
drafts: ?(drafts ? Some(true) : None),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The document for a set of environments. Comments are not kept: this writes a
|
|
161
|
+
new file, it does not edit one. */
|
|
162
|
+
let print = (environments: t): string =>
|
|
163
|
+
{environments: environments->Array.map(entryOf)}->Util_Sury.toJson(fileSchema)->stringifyYaml
|
|
164
|
+
|
|
165
|
+
/** Where a deploy looks for it: beside the deploy manifest. */
|
|
166
|
+
let fileName = "environments.yaml"
|
|
167
|
+
|
|
168
|
+
let pathBeside = (~manifest: string): string =>
|
|
169
|
+
NodePath.join([NodePath.dirname(manifest), fileName])
|
|
170
|
+
|
|
171
|
+
// ── Resolving ────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
A branch or tag pattern as GitHub's workflow filters spell them: `*` matches
|
|
175
|
+
within one path segment, `**` across segments. Nothing else is special.
|
|
176
|
+
*/
|
|
177
|
+
let matches = (~pattern: string, name: string): bool => {
|
|
178
|
+
let escaped =
|
|
179
|
+
pattern
|
|
180
|
+
->String.split("**")
|
|
181
|
+
->Array.map(part =>
|
|
182
|
+
part
|
|
183
|
+
->String.replaceRegExp(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
184
|
+
->String.replaceAll("*", "[^/]*")
|
|
185
|
+
)
|
|
186
|
+
->Array.join(".*")
|
|
187
|
+
RegExp.fromString(`^${escaped}$`)->RegExp.test(name)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** What a push or a tag deploys to. */
|
|
191
|
+
type ref_ =
|
|
192
|
+
| Branch(string)
|
|
193
|
+
| Tag(string)
|
|
194
|
+
|
|
195
|
+
/** `refs/heads/test` and `refs/tags/v1`; anything else is not deployable. */
|
|
196
|
+
let refOf = (ref: string): option<ref_> =>
|
|
197
|
+
if ref->String.startsWith("refs/heads/") {
|
|
198
|
+
Some(Branch(ref->String.slice(~start=11)))
|
|
199
|
+
} else if ref->String.startsWith("refs/tags/") {
|
|
200
|
+
Some(Tag(ref->String.slice(~start=10)))
|
|
201
|
+
} else {
|
|
202
|
+
None
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
type resolved = {name: string, stack: string, purpose: purpose}
|
|
206
|
+
|
|
207
|
+
/** The first shared environment, in file order, that the branch or tag feeds. */
|
|
208
|
+
let resolve = (environments: t, ref: ref_): option<resolved> =>
|
|
209
|
+
environments->Array.findMap(({name, purpose, feed}) =>
|
|
210
|
+
switch (feed, ref) {
|
|
211
|
+
| (Shared({branch: Some(pattern), stack}), Branch(branch)) if matches(~pattern, branch) =>
|
|
212
|
+
Some({name, stack, purpose})
|
|
213
|
+
| (Shared({tags: Some(pattern), stack}), Tag(tag)) if matches(~pattern, tag) =>
|
|
214
|
+
Some({name, stack, purpose})
|
|
215
|
+
| (Shared(_), _) | (ReviewOf(_), _) => None
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
/** The review stack of one pull request, if the branch it targets has a review
|
|
220
|
+
environment. `draft` pull requests get one only where the environment asks. */
|
|
221
|
+
let resolvePullRequest = (environments: t, ~base: string, ~number: int, ~draft: bool): option<
|
|
222
|
+
resolved,
|
|
223
|
+
> =>
|
|
224
|
+
environments->Array.findMap(({name, purpose, feed}) =>
|
|
225
|
+
switch feed {
|
|
226
|
+
| ReviewOf({into, drafts}) if matches(~pattern=into, base) && (drafts || !draft) =>
|
|
227
|
+
Some({name, stack: `${name}-pr-${number->Int.toString}`, purpose})
|
|
228
|
+
| ReviewOf(_) | Shared(_) => None
|
|
229
|
+
}
|
|
230
|
+
)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Yaml from "yaml";
|
|
5
|
+
import * as Nodefs from "node:fs";
|
|
6
|
+
import * as Nodepath from "node:path";
|
|
7
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
8
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
11
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
12
|
+
|
|
13
|
+
let purposeSchema = Sury.union([
|
|
14
|
+
Sury.shape(Sury.literal("dev"), param => "Dev"),
|
|
15
|
+
Sury.shape(Sury.literal("test"), param => "Test"),
|
|
16
|
+
Sury.shape(Sury.literal("production"), param => "Production"),
|
|
17
|
+
Sury.shape(Sury.literal("demo"), param => "Demo"),
|
|
18
|
+
Sury.shape(Sury.literal("review"), param => "Review"),
|
|
19
|
+
Sury.shape(Sury.string, other => ({
|
|
20
|
+
TAG: "Other",
|
|
21
|
+
_0: other
|
|
22
|
+
}))
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
function purposeName(purpose) {
|
|
26
|
+
if (typeof purpose === "object") {
|
|
27
|
+
return purpose._0;
|
|
28
|
+
}
|
|
29
|
+
switch (purpose) {
|
|
30
|
+
case "Dev" :
|
|
31
|
+
return "dev";
|
|
32
|
+
case "Test" :
|
|
33
|
+
return "test";
|
|
34
|
+
case "Production" :
|
|
35
|
+
return "production";
|
|
36
|
+
case "Demo" :
|
|
37
|
+
return "demo";
|
|
38
|
+
case "Review" :
|
|
39
|
+
return "review";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let entrySchema = Sury.$schema(s => ({
|
|
44
|
+
name: s.m(Sury.string),
|
|
45
|
+
purpose: s.m(purposeSchema),
|
|
46
|
+
branch: s.m(Sury.$option(Sury.string)),
|
|
47
|
+
tags: s.m(Sury.$option(Sury.string)),
|
|
48
|
+
stack: s.m(Sury.$option(Sury.string)),
|
|
49
|
+
pullRequestsInto: s.m(Sury.$option(Sury.string)),
|
|
50
|
+
drafts: s.m(Sury.$option(Sury.boolean))
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
let fileSchema = Sury.$schema(s => ({
|
|
54
|
+
environments: s.m(Sury.array(entrySchema))
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
function environmentOf(entry) {
|
|
58
|
+
let match = entry.pullRequestsInto;
|
|
59
|
+
let match$1 = entry.branch;
|
|
60
|
+
let match$2 = entry.tags;
|
|
61
|
+
if (match !== undefined) {
|
|
62
|
+
if (match$1 === undefined && match$2 === undefined) {
|
|
63
|
+
if (Stdlib_Option.isSome(entry.stack)) {
|
|
64
|
+
return {
|
|
65
|
+
TAG: "Error",
|
|
66
|
+
_0: entry.name + `: a review environment names its stacks itself; remove stack`
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Ok",
|
|
71
|
+
_0: {
|
|
72
|
+
name: entry.name,
|
|
73
|
+
purpose: entry.purpose,
|
|
74
|
+
feed: {
|
|
75
|
+
TAG: "ReviewOf",
|
|
76
|
+
into: match,
|
|
77
|
+
drafts: Stdlib_Option.getOr(entry.drafts, false)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
let exit = 0;
|
|
85
|
+
if (match$1 !== undefined) {
|
|
86
|
+
exit = 2;
|
|
87
|
+
} else {
|
|
88
|
+
if (match$2 === undefined) {
|
|
89
|
+
return {
|
|
90
|
+
TAG: "Error",
|
|
91
|
+
_0: entry.name + `: says neither branch, tags nor pullRequestsInto, so nothing feeds it`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
exit = 2;
|
|
95
|
+
}
|
|
96
|
+
if (exit === 2) {
|
|
97
|
+
if (Stdlib_Option.isSome(entry.drafts)) {
|
|
98
|
+
return {
|
|
99
|
+
TAG: "Error",
|
|
100
|
+
_0: entry.name + `: drafts is only meaningful with pullRequestsInto`
|
|
101
|
+
};
|
|
102
|
+
} else {
|
|
103
|
+
return {
|
|
104
|
+
TAG: "Ok",
|
|
105
|
+
_0: {
|
|
106
|
+
name: entry.name,
|
|
107
|
+
purpose: entry.purpose,
|
|
108
|
+
feed: {
|
|
109
|
+
TAG: "Shared",
|
|
110
|
+
branch: match$1,
|
|
111
|
+
tags: match$2,
|
|
112
|
+
stack: Stdlib_Option.getOr(entry.stack, entry.name)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
TAG: "Error",
|
|
121
|
+
_0: entry.name + `: pullRequestsInto cannot be combined with branch or tags`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseString(yamlText) {
|
|
126
|
+
let e;
|
|
127
|
+
try {
|
|
128
|
+
e = {
|
|
129
|
+
TAG: "Ok",
|
|
130
|
+
_0: Sury.parseOrThrow(Yaml.parse(yamlText), fileSchema)
|
|
131
|
+
};
|
|
132
|
+
} catch (raw_err) {
|
|
133
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
134
|
+
e = err.RE_EXN_ID === "JsExn" ? ({
|
|
135
|
+
TAG: "Error",
|
|
136
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "YAML parse error")
|
|
137
|
+
}) : ({
|
|
138
|
+
TAG: "Error",
|
|
139
|
+
_0: "YAML parse error"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (e.TAG !== "Ok") {
|
|
143
|
+
return e;
|
|
144
|
+
}
|
|
145
|
+
let environments = e._0.environments;
|
|
146
|
+
let names = environments.map(e => e.name);
|
|
147
|
+
let duplicate = names.find(n => names.filter(m => m === n).length > 1);
|
|
148
|
+
if (duplicate !== undefined) {
|
|
149
|
+
return {
|
|
150
|
+
TAG: "Error",
|
|
151
|
+
_0: duplicate + `: named twice`
|
|
152
|
+
};
|
|
153
|
+
} else {
|
|
154
|
+
return Stdlib_Array.reduce(environments, {
|
|
155
|
+
TAG: "Ok",
|
|
156
|
+
_0: []
|
|
157
|
+
}, (acc, entry) => {
|
|
158
|
+
let match = environmentOf(entry);
|
|
159
|
+
if (acc.TAG === "Ok") {
|
|
160
|
+
if (match.TAG === "Ok") {
|
|
161
|
+
return {
|
|
162
|
+
TAG: "Ok",
|
|
163
|
+
_0: acc._0.concat([match._0])
|
|
164
|
+
};
|
|
165
|
+
} else {
|
|
166
|
+
return {
|
|
167
|
+
TAG: "Error",
|
|
168
|
+
_0: match._0
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
return acc;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseFile(path) {
|
|
179
|
+
try {
|
|
180
|
+
return parseString(Nodefs.readFileSync(path, "utf8"));
|
|
181
|
+
} catch (raw_err) {
|
|
182
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
183
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
184
|
+
return {
|
|
185
|
+
TAG: "Error",
|
|
186
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), `Cannot read ` + path)
|
|
187
|
+
};
|
|
188
|
+
} else {
|
|
189
|
+
return {
|
|
190
|
+
TAG: "Error",
|
|
191
|
+
_0: `Cannot read ` + path
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function entryOf(environment) {
|
|
198
|
+
let match = environment.feed;
|
|
199
|
+
if (match.TAG !== "Shared") {
|
|
200
|
+
return {
|
|
201
|
+
name: environment.name,
|
|
202
|
+
purpose: environment.purpose,
|
|
203
|
+
pullRequestsInto: match.into,
|
|
204
|
+
drafts: match.drafts ? true : undefined
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
let stack = match.stack;
|
|
208
|
+
return {
|
|
209
|
+
name: environment.name,
|
|
210
|
+
purpose: environment.purpose,
|
|
211
|
+
branch: match.branch,
|
|
212
|
+
tags: match.tags,
|
|
213
|
+
stack: stack === environment.name ? undefined : stack
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function print(environments) {
|
|
218
|
+
return Yaml.stringify(Util_Sury$Reventless.toJson({
|
|
219
|
+
environments: environments.map(entryOf)
|
|
220
|
+
}, fileSchema));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let fileName = "environments.yaml";
|
|
224
|
+
|
|
225
|
+
function pathBeside(manifest) {
|
|
226
|
+
return Nodepath.join(Nodepath.dirname(manifest), fileName);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function matches(pattern, name) {
|
|
230
|
+
let escaped = pattern.split("**").map(part => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", "[^/]*")).join(".*");
|
|
231
|
+
return new RegExp(`^` + escaped + `$`).test(name);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function refOf(ref) {
|
|
235
|
+
if (ref.startsWith("refs/heads/")) {
|
|
236
|
+
return {
|
|
237
|
+
TAG: "Branch",
|
|
238
|
+
_0: ref.slice(11)
|
|
239
|
+
};
|
|
240
|
+
} else if (ref.startsWith("refs/tags/")) {
|
|
241
|
+
return {
|
|
242
|
+
TAG: "Tag",
|
|
243
|
+
_0: ref.slice(10)
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolve(environments, ref) {
|
|
251
|
+
return Stdlib_Array.findMap(environments, param => {
|
|
252
|
+
let feed = param.feed;
|
|
253
|
+
let purpose = param.purpose;
|
|
254
|
+
let name = param.name;
|
|
255
|
+
if (feed.TAG !== "Shared") {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
let pattern = feed.branch;
|
|
259
|
+
if (pattern !== undefined && ref.TAG === "Branch") {
|
|
260
|
+
if (matches(pattern, ref._0)) {
|
|
261
|
+
return {
|
|
262
|
+
name: name,
|
|
263
|
+
stack: feed.stack,
|
|
264
|
+
purpose: purpose
|
|
265
|
+
};
|
|
266
|
+
} else {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
let pattern$1 = feed.tags;
|
|
271
|
+
if (pattern$1 !== undefined && ref.TAG !== "Branch" && matches(pattern$1, ref._0)) {
|
|
272
|
+
return {
|
|
273
|
+
name: name,
|
|
274
|
+
stack: feed.stack,
|
|
275
|
+
purpose: purpose
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolvePullRequest(environments, base, number, draft) {
|
|
282
|
+
return Stdlib_Array.findMap(environments, param => {
|
|
283
|
+
let feed = param.feed;
|
|
284
|
+
let name = param.name;
|
|
285
|
+
if (feed.TAG === "Shared" || !(matches(feed.into, base) && (feed.drafts || !draft))) {
|
|
286
|
+
return;
|
|
287
|
+
} else {
|
|
288
|
+
return {
|
|
289
|
+
name: name,
|
|
290
|
+
stack: name + `-pr-` + number.toString(),
|
|
291
|
+
purpose: param.purpose
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export {
|
|
298
|
+
purposeSchema,
|
|
299
|
+
purposeName,
|
|
300
|
+
entrySchema,
|
|
301
|
+
fileSchema,
|
|
302
|
+
environmentOf,
|
|
303
|
+
parseString,
|
|
304
|
+
parseFile,
|
|
305
|
+
entryOf,
|
|
306
|
+
print,
|
|
307
|
+
fileName,
|
|
308
|
+
pathBeside,
|
|
309
|
+
matches,
|
|
310
|
+
refOf,
|
|
311
|
+
resolve,
|
|
312
|
+
resolvePullRequest,
|
|
313
|
+
}
|
|
314
|
+
/* purposeSchema Not a pure module */
|
package/src/types/Message.res
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
@schema
|
|
3
3
|
type service = string
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
The issuing caller, classified for `@owner` enforcement (`OwnerScope.toClaim`).
|
|
7
|
+
|
|
8
|
+
Written only by the command generator, from the authenticated identity, so a
|
|
9
|
+
handler can tell an owner from an operator without seeing the caller's groups.
|
|
10
|
+
Absent on every command that did not come through the generator — an
|
|
11
|
+
automation's follow-up, an internal dispatch — which a handler reads as the
|
|
12
|
+
platform acting for itself.
|
|
13
|
+
*/
|
|
14
|
+
module CallerClaim = {
|
|
15
|
+
@schema
|
|
16
|
+
type t =
|
|
17
|
+
| Owned({userId: string})
|
|
18
|
+
| Exempt
|
|
19
|
+
| Unidentified
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
/**
|
|
6
23
|
Envelope metadata attached to every event and command.
|
|
7
24
|
|
|
@@ -50,6 +67,10 @@ type meta = {
|
|
|
50
67
|
/** Extensible header bag for cross-cutting context (tenantId, feature flags, etc.).
|
|
51
68
|
Absent when empty — consumers normalise with `->Option.getOr(Dict.make())`. */
|
|
52
69
|
headers?: dict<string>,
|
|
70
|
+
/** Commands only: the caller as the generator classified it. Deliberately not
|
|
71
|
+
inherited by `deriveMeta`, so an event or a follow-up command never carries
|
|
72
|
+
the claim of the command that caused it. */
|
|
73
|
+
callerClaim?: CallerClaim.t,
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/**
|
|
@@ -6,6 +6,19 @@ import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
|
6
6
|
|
|
7
7
|
let serviceSchema = Sury.string;
|
|
8
8
|
|
|
9
|
+
let schema = Sury.union([
|
|
10
|
+
Sury.$schema(s => ({
|
|
11
|
+
TAG: "Owned",
|
|
12
|
+
userId: s.m(Sury.string)
|
|
13
|
+
})),
|
|
14
|
+
Sury.literal("Exempt"),
|
|
15
|
+
Sury.literal("Unidentified")
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
let CallerClaim = {
|
|
19
|
+
schema: schema
|
|
20
|
+
};
|
|
21
|
+
|
|
9
22
|
let metaSchema = Sury.$schema(s => ({
|
|
10
23
|
service: s.m(serviceSchema),
|
|
11
24
|
time: s.m(Sury.string),
|
|
@@ -16,7 +29,8 @@ let metaSchema = Sury.$schema(s => ({
|
|
|
16
29
|
causationId: s.m(Sury.$option(Sury.string)),
|
|
17
30
|
traceparent: s.m(Sury.$option(Sury.string)),
|
|
18
31
|
schemaVersion: s.m(Sury.$option(Sury.string)),
|
|
19
|
-
headers: s.m(Sury.$option(Sury.record(Sury.string)))
|
|
32
|
+
headers: s.m(Sury.$option(Sury.record(Sury.string))),
|
|
33
|
+
callerClaim: s.m(Sury.$option(schema))
|
|
20
34
|
}));
|
|
21
35
|
|
|
22
36
|
let contextSchema = Sury.$schema(s => ({
|
|
@@ -189,6 +203,7 @@ function composeEventJson$p(id, meta, eventJson) {
|
|
|
189
203
|
|
|
190
204
|
export {
|
|
191
205
|
serviceSchema,
|
|
206
|
+
CallerClaim,
|
|
192
207
|
metaSchema,
|
|
193
208
|
contextSchema,
|
|
194
209
|
statusChangeSchema,
|