@pithy-sh/core 0.7.3 → 0.8.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/dist/naming/domains.d.ts +43 -3
- package/dist/naming/domains.d.ts.map +1 -1
- package/dist/naming/domains.js +62 -4
- package/dist/naming/feature.d.ts +126 -11
- package/dist/naming/feature.d.ts.map +1 -1
- package/dist/naming/feature.js +184 -30
- package/dist/naming/limits.d.ts +23 -7
- package/dist/naming/limits.d.ts.map +1 -1
- package/dist/naming/limits.js +22 -9
- package/dist/naming/provisionScope.d.ts +25 -11
- package/dist/naming/provisionScope.d.ts.map +1 -1
- package/dist/naming/provisionScope.js +28 -11
- package/dist/seed/seed.d.ts +36 -7
- package/dist/seed/seed.d.ts.map +1 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/dist/worker/identity.d.ts +9 -0
- package/dist/worker/identity.d.ts.map +1 -1
- package/dist/worker/identity.js +10 -1
- package/dist/workflow/faults.d.ts +34 -2
- package/dist/workflow/faults.d.ts.map +1 -1
- package/dist/workflow/faults.js +42 -18
- package/dist/workflow/host.d.ts +7 -0
- package/dist/workflow/host.d.ts.map +1 -1
- package/dist/workflow/host.js +5 -3
- package/dist/workflow/naming.d.ts +8 -0
- package/dist/workflow/naming.d.ts.map +1 -1
- package/dist/workflow/naming.js +15 -0
- package/package.json +1 -1
- package/src/naming/domains.ts +75 -5
- package/src/naming/feature.ts +274 -38
- package/src/naming/limits.ts +25 -8
- package/src/naming/provisionScope.ts +39 -14
- package/src/seed/seed.ts +36 -7
- package/src/version.generated.ts +1 -1
- package/src/worker/identity.ts +10 -0
- package/src/workflow/faults.ts +61 -4
- package/src/workflow/host.ts +15 -3
- package/src/workflow/naming.ts +25 -0
package/dist/naming/feature.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
3
|
import { ValidationError } from "../error/pithyError.js";
|
|
4
|
-
import { featureMarker } from "./environment.js";
|
|
4
|
+
import { featureMarker, isFeatureMarker } from "./environment.js";
|
|
5
5
|
import { NAMESPACE_LIMITS } from "./limits.js";
|
|
6
|
-
import { assertValidProjectName,
|
|
6
|
+
import { assertValidProjectName, kebab } from "./resource.js";
|
|
7
7
|
//#region src/naming/feature.ts
|
|
8
8
|
/** Every feature resource kind, for tests and for callers that enumerate. */
|
|
9
9
|
const FEATURE_RESOURCE_KINDS = [
|
|
@@ -11,8 +11,6 @@ const FEATURE_RESOURCE_KINDS = [
|
|
|
11
11
|
"kv",
|
|
12
12
|
"r2"
|
|
13
13
|
];
|
|
14
|
-
/** Below this the slug segment isn't worth keeping legible; the trailing segment is truncated to give it room. */
|
|
15
|
-
const MIN_SLUG_BUDGET = 3;
|
|
16
14
|
/**
|
|
17
15
|
* Refuse an issue number the budgets were not derived against.
|
|
18
16
|
*
|
|
@@ -29,21 +27,106 @@ function assertIssue(issue) {
|
|
|
29
27
|
detail: `Feature resource names reserve 6 digits, and every project-name budget is derived against that.`
|
|
30
28
|
});
|
|
31
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* **The issue as a name carries it: its digits, without leading zeros (#643).** `feature/0643-foo` and
|
|
32
|
+
* `feature/643-foo` are one issue, so they are one feature and compose one set of names. Read as strings they
|
|
33
|
+
* were two features, and their rate-limit namespaces collided every time. `0` stays `0`.
|
|
34
|
+
*/
|
|
35
|
+
function canonicalIssue(issue) {
|
|
36
|
+
assertIssue(issue);
|
|
37
|
+
return issue.replace(/^0+(?=[0-9])/, "");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The `f<digits>` segment a project name carries, or `null` — the shape a feature's issue takes (#643).
|
|
41
|
+
*
|
|
42
|
+
* Read after kebabbing, because the kebab is what reaches an account.
|
|
43
|
+
*/
|
|
44
|
+
function featureMarkerInProjectName(project) {
|
|
45
|
+
return kebab(project).split("-").find(isFeatureMarker) ?? null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* **Refuse features for a project whose name carries an `f<digits>` segment (#643).**
|
|
49
|
+
*
|
|
50
|
+
* A feature name is read left to right: the project, then the first `f<digits>` segment, which is the issue.
|
|
51
|
+
* A project named `acme-f12-x` puts that segment inside itself, and then its feature 3 on `y` and project `acme`'s
|
|
52
|
+
* feature 12 on `x-f3-y` are one string. Environment names already obey this rule ({@link isFeatureMarker}), so
|
|
53
|
+
* the project obeys it too, where the two shapes meet.
|
|
54
|
+
*
|
|
55
|
+
* **Refused here, not at `requireProjectName`.** Such a project's staging and prod are untouched: no name they
|
|
56
|
+
* compose holds {@link FEATURE_TAIL_SEPARATOR}, so no feature anywhere can compose one of theirs. Refusing the
|
|
57
|
+
* name everywhere would break every command of a project that is otherwise sound. `pithy init` refuses a new one,
|
|
58
|
+
* and `pithy doctor` names the segment an existing one carries. Nothing is renamed.
|
|
59
|
+
*/
|
|
60
|
+
function assertFeatureProject(project) {
|
|
61
|
+
const marker = featureMarkerInProjectName(project);
|
|
62
|
+
if (marker === null) return;
|
|
63
|
+
throw new ValidationError({
|
|
64
|
+
message: `"${project}" can't have feature environments. Its name carries ${marker}, the shape a feature's issue takes.`,
|
|
65
|
+
action: "Staging and prod are unaffected. Features need a project name with no f and a number as one segment.",
|
|
66
|
+
detail: `A feature name is <project>-f<issue>-<slug>--<thing>, read from the first f<digits> segment. With one inside "${kebab(project)}", another project's feature could compose this project's feature names.`
|
|
67
|
+
});
|
|
68
|
+
}
|
|
32
69
|
/** The `<project>-f<issue>` head both feature shapes share, with the project held to the one project rule. */
|
|
33
70
|
function head(identity) {
|
|
34
71
|
assertValidProjectName(identity.project);
|
|
35
|
-
|
|
36
|
-
return `${kebab(identity.project)}-${featureMarker(identity.issue)}`;
|
|
72
|
+
assertFeatureProject(identity.project);
|
|
73
|
+
return `${kebab(identity.project)}-${featureMarker(canonicalIssue(identity.issue))}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* **Read a feature name back into its owner, or `null` when it is not one (#643).**
|
|
77
|
+
*
|
|
78
|
+
* Exact, because the shape leaves one reading: a feature name holds {@link FEATURE_TAIL_SEPARATOR} exactly once,
|
|
79
|
+
* everything before it is `<project>-f<issue>-<slug>`, and the first `f<digits>` segment there is the issue,
|
|
80
|
+
* since a project may carry none. Nothing else Pithy composes holds the separator at all.
|
|
81
|
+
*/
|
|
82
|
+
function parseFeatureName(name) {
|
|
83
|
+
const at = name.indexOf("--");
|
|
84
|
+
if (at < 0 || name.indexOf("--", at + 1) >= 0) return null;
|
|
85
|
+
const thing = name.slice(at + 2);
|
|
86
|
+
const segments = name.slice(0, at).split("-");
|
|
87
|
+
const marker = segments.findIndex(isFeatureMarker);
|
|
88
|
+
if (thing === "" || marker < 1 || marker === segments.length - 1) return null;
|
|
89
|
+
if (segments.some((segment) => segment === "")) return null;
|
|
90
|
+
return {
|
|
91
|
+
project: segments.slice(0, marker).join("-"),
|
|
92
|
+
issue: segments[marker].slice(1).replace(/^0+(?=[0-9])/, ""),
|
|
93
|
+
slug: segments.slice(marker + 1).join("-"),
|
|
94
|
+
thing
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* **Is this name one this feature composes?** The ownership check the isolation gates ask (#643).
|
|
99
|
+
*
|
|
100
|
+
* Parsed, never prefix-matched, and exact: the name's project, issue and slug are this feature's, character for
|
|
101
|
+
* character. `<project>-f<issue>-` is shared by every branch of one issue, so a prefix match reached siblings; and
|
|
102
|
+
* a fitted form of the slug, which this used to accept, is a short hash some sibling can have as its whole slug
|
|
103
|
+
* (`feature/12-login` owned `feature/12-c`). No feature name is fitted any more ({@link composeFeatureName}), so
|
|
104
|
+
* there is nothing but the slug itself to accept.
|
|
105
|
+
*/
|
|
106
|
+
function isFeatureOwnedName(identity, name) {
|
|
107
|
+
const parsed = parseFeatureName(name);
|
|
108
|
+
if (parsed === null) return false;
|
|
109
|
+
return parsed.project === kebab(identity.project) && parsed.issue === canonicalIssue(identity.issue) && parsed.slug === kebab(identity.slug);
|
|
37
110
|
}
|
|
38
111
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
112
|
+
* **The longest slug one feature name leaves room for** — `limit` less `<project>-f<issue>-`, the
|
|
113
|
+
* {@link FEATURE_TAIL_SEPARATOR}, and `thing`, which is everything after it. Zero or less when `thing` leaves none.
|
|
114
|
+
*/
|
|
115
|
+
function featureSlugRoom(parts, thing, limit) {
|
|
116
|
+
return limit - head(parts).length - 1 - 2 - thing.length;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Compose `<head>-<slug>--<tail><fixed>` whole, or refuse (#643).
|
|
42
120
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
121
|
+
* **Never truncated.** A fitted slug was a short hash, and a hash is a slug some sibling branch can have: the
|
|
122
|
+
* review of 4828e1fc found `feature/12-login` owning `feature/12-c`'s names, and two long slugs of one issue
|
|
123
|
+
* composing one database. A name that does not fit is refused, naming the longest slug that does, and
|
|
124
|
+
* `pithy feature create` and `pithy provision --feature` refuse the branch before anything exists
|
|
125
|
+
* ({@link assertFeatureSlugFits}). This is the backstop behind them: no path composes a feature name that is not
|
|
126
|
+
* exactly its feature's.
|
|
45
127
|
*/
|
|
46
|
-
function composeFeatureName(
|
|
128
|
+
function composeFeatureName(parts, slug, tail, fixed, limit, label) {
|
|
129
|
+
const headSegment = head(parts);
|
|
47
130
|
for (const [role, value] of [["slug", slug], ["binding or worker", tail]]) {
|
|
48
131
|
if (value) continue;
|
|
49
132
|
throw new ValidationError({
|
|
@@ -52,29 +135,76 @@ function composeFeatureName(headSegment, slug, tail, fixed, budget) {
|
|
|
52
135
|
detail: `composeFeatureName received an empty ${role} segment for "${headSegment}".`
|
|
53
136
|
});
|
|
54
137
|
}
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
138
|
+
const thing = `${tail}${fixed}`;
|
|
139
|
+
const room = featureSlugRoom(parts, thing, limit);
|
|
140
|
+
if (room < 1) throw new ValidationError({
|
|
141
|
+
message: `"${thing}" is too long for a feature's ${label.replace(/^an? /, "")}.`,
|
|
142
|
+
action: `Shorten it. ${label[0]?.toUpperCase()}${label.slice(1)} stops at ${limit}, and ${headSegment}-<slug>-- comes first.`,
|
|
143
|
+
detail: `${headSegment}-<slug>--${thing} leaves ${room} characters for a slug.`
|
|
144
|
+
});
|
|
145
|
+
if (slug.length > room) throw new ValidationError({
|
|
146
|
+
message: `Slug "${slug}" is ${slug.length} characters. ${label[0]?.toUpperCase()}${label.slice(1)} ${headSegment}-<slug>--${thing} leaves room for at most ${room}.`,
|
|
147
|
+
action: "Use a shorter branch slug.",
|
|
148
|
+
detail: `${headSegment}-${slug}--${thing} is ${headSegment.length + 1 + slug.length + 2 + thing.length} characters; the limit is ${limit}. Feature names are never truncated.`
|
|
149
|
+
});
|
|
150
|
+
return `${headSegment}-${slug}--${thing}`;
|
|
151
|
+
}
|
|
152
|
+
/** The longest slug every one of `shapes` leaves room for, for this project and issue, and the shape that sets it. */
|
|
153
|
+
function tightestFeatureShape(parts, shapes) {
|
|
154
|
+
let max = Number.POSITIVE_INFINITY;
|
|
155
|
+
let shape = null;
|
|
156
|
+
for (const candidate of shapes) {
|
|
157
|
+
const room = featureSlugRoom(parts, candidate.thing, candidate.limit);
|
|
158
|
+
if (room < max) {
|
|
159
|
+
max = room;
|
|
160
|
+
shape = candidate;
|
|
161
|
+
}
|
|
62
162
|
}
|
|
63
|
-
return
|
|
163
|
+
return {
|
|
164
|
+
max,
|
|
165
|
+
shape
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The longest slug a feature of this project and issue may have, across every name it composes. */
|
|
169
|
+
function maxFeatureSlug(parts, shapes) {
|
|
170
|
+
return tightestFeatureShape(parts, shapes).max;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* **Refuse a branch whose slug cannot fit every name its feature composes (#643).** The one refusal
|
|
174
|
+
* `pithy feature create` and `pithy provision --feature` make, before a worktree or a resource exists, with the
|
|
175
|
+
* project's maximum in the message. {@link composeFeatureName} refuses the same slug one name at a time; this says
|
|
176
|
+
* it once, for all of them.
|
|
177
|
+
*/
|
|
178
|
+
function assertFeatureSlugFits(identity, shapes) {
|
|
179
|
+
const { max, shape } = tightestFeatureShape(identity, shapes);
|
|
180
|
+
const slug = kebab(identity.slug);
|
|
181
|
+
if (shape === null || slug.length <= max) return;
|
|
182
|
+
const issue = canonicalIssue(identity.issue);
|
|
183
|
+
const project = kebab(identity.project);
|
|
184
|
+
if (max < 1) throw new ValidationError({
|
|
185
|
+
message: `"${shape.thing}" leaves no room for a slug in ${project}'s features.`,
|
|
186
|
+
action: `Shorten it. ${shape.label[0]?.toUpperCase()}${shape.label.slice(1)} stops at ${shape.limit}.`,
|
|
187
|
+
detail: `${head(identity)}-<slug>--${shape.thing} leaves ${max} characters for a slug.`
|
|
188
|
+
});
|
|
189
|
+
throw new ValidationError({
|
|
190
|
+
message: `Slug "${slug}" is ${slug.length} characters. Feature slugs in ${project} stop at ${max} characters at issue ${issue}.`,
|
|
191
|
+
action: `Name the branch feature/${issue}-<slug of ${max} or fewer>. Feature names are never truncated.`,
|
|
192
|
+
detail: `${shape.label} ${head(identity)}-<slug>--${shape.thing} is held to ${shape.limit} characters.`
|
|
193
|
+
});
|
|
64
194
|
}
|
|
65
195
|
/**
|
|
66
|
-
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug
|
|
196
|
+
* The full Cloudflare resource name for a feature's binding — `<project>-f<issue>-<slug>--<binding>-<kind>`.
|
|
67
197
|
*
|
|
68
198
|
* Held to **R2's 63**, the strictest of the three kinds a feature provisions, so one shape is legal for
|
|
69
199
|
* all of them: lowercase, hyphenated, alphanumeric at both ends. A D1 or KV name could be longer, but a
|
|
70
200
|
* feature that provisions a bucket and a database wants them recognizably the same name.
|
|
71
201
|
*/
|
|
72
202
|
function featureResourceName(identity, binding, kind) {
|
|
73
|
-
return composeFeatureName(
|
|
203
|
+
return composeFeatureName(identity, kebab(identity.slug), kebab(binding), `-${kind}`, NAMESPACE_LIMITS.r2.maxLength, NAMESPACE_LIMITS.r2.label);
|
|
74
204
|
}
|
|
75
205
|
/**
|
|
76
206
|
* The CF Secrets Store entry name holding one of a feature's **environment-scoped** secrets —
|
|
77
|
-
* `<project>-f<issue>-<slug
|
|
207
|
+
* `<project>-f<issue>-<slug>--<secret>`.
|
|
78
208
|
*
|
|
79
209
|
* A Cloudflare account has one Secrets Store, flat and unpartitionable, so the entry name is the only
|
|
80
210
|
* partition there is. A feature therefore needs its own names for the same reason it needs its own
|
|
@@ -84,14 +214,14 @@ function featureResourceName(identity, binding, kind) {
|
|
|
84
214
|
* no documented Cloudflare cap, and holding it to the strictest kind's would hash
|
|
85
215
|
* `secrets-encryption-keys` down to nothing for no reason.
|
|
86
216
|
*
|
|
87
|
-
* A `global` secret is
|
|
88
|
-
*
|
|
217
|
+
* A `global` secret is named here too since #643: a feature shares nothing with any other environment, so
|
|
218
|
+
* its entry for a global secret is its own rather than the project's `<project>-global-<secret>`.
|
|
89
219
|
*/
|
|
90
220
|
function featureSecretEntryName(identity, secret) {
|
|
91
|
-
return composeFeatureName(
|
|
221
|
+
return composeFeatureName(identity, kebab(identity.slug), kebab(secret), "", NAMESPACE_LIMITS.secretEntry.maxLength, NAMESPACE_LIMITS.secretEntry.label);
|
|
92
222
|
}
|
|
93
223
|
/**
|
|
94
|
-
* The Worker **script name** for one of a feature's workers — `<project>-f<issue>-<slug
|
|
224
|
+
* The Worker **script name** for one of a feature's workers — `<project>-f<issue>-<slug>--<worker>`.
|
|
95
225
|
*
|
|
96
226
|
* This is the name the feature's Workers deploy under, and therefore the name a sibling's `service`
|
|
97
227
|
* binding must target, so RPC inside a feature environment reaches that feature's deployment rather than
|
|
@@ -103,13 +233,37 @@ function featureSecretEntryName(identity, secret) {
|
|
|
103
233
|
* has a charset rule and no length rule, so nothing upstream was going to stop it either.
|
|
104
234
|
*
|
|
105
235
|
* **`app` is the `apps/<app>` directory, never the deploy name.** A scaffolded Worker deploys as
|
|
106
|
-
* `<project>-<app>`, and handing that here composed `<project>-f<issue>-<slug
|
|
236
|
+
* `<project>-<app>`, and handing that here composed `<project>-f<issue>-<slug>--<project>-<app>` — the
|
|
107
237
|
* project twice, spent out of the budget above (#587). This function takes a string and cannot tell the
|
|
108
238
|
* two apart, so the provisioning path reaches it only through `featureScope`, which is handed both names
|
|
109
239
|
* and picks.
|
|
110
240
|
*/
|
|
111
241
|
function featureWorkerName(identity, app) {
|
|
112
|
-
return composeFeatureName(
|
|
242
|
+
return composeFeatureName(identity, kebab(identity.slug), kebab(app), "", NAMESPACE_LIMITS.worker.maxLength, NAMESPACE_LIMITS.worker.label);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* The deployed name of one of a feature's **Workflows** — `<project>-f<issue>-<slug>--<capability>-<job>` (#643).
|
|
246
|
+
*
|
|
247
|
+
* A Workflow name is account-wide, like a Worker's, so a feature's email host cannot run
|
|
248
|
+
* `<project>-feature-email-send`: every open branch would deploy the same Workflow over every other's. It takes
|
|
249
|
+
* the feature's head instead, exactly as its host Worker does ({@link featureWorkerName}), so the Worker and the
|
|
250
|
+
* Workflows it hosts are recognizably one feature's and teardown recomputes both from the identity.
|
|
251
|
+
*
|
|
252
|
+
* Held to the **Workflow** limit ({@link NAMESPACE_LIMITS.workflow}), and fitted the way every feature name is:
|
|
253
|
+
* the slug gives way first, then the `<capability>-<job>` tail, deterministically.
|
|
254
|
+
*/
|
|
255
|
+
function featureWorkflowName(identity, capability, job) {
|
|
256
|
+
return composeFeatureName(identity, kebab(identity.slug), `${kebab(capability)}-${kebab(job)}`, "", NAMESPACE_LIMITS.workflow.maxLength, NAMESPACE_LIMITS.workflow.label);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The name of one of a feature's **Vectorize indexes** — `<project>-f<issue>-<slug>--<thing>` (#643).
|
|
260
|
+
*
|
|
261
|
+
* An index name is account-wide, so a feature's vector host cannot bind `<project>-feature-vector-<index>`: every
|
|
262
|
+
* open branch would read and write one index. Held to the Vectorize limit and fitted the way every feature name
|
|
263
|
+
* is, so teardown recomputes it from the identity.
|
|
264
|
+
*/
|
|
265
|
+
function featureVectorizeIndexName(identity, thing) {
|
|
266
|
+
return composeFeatureName(identity, kebab(identity.slug), kebab(thing), "", NAMESPACE_LIMITS.vectorizeIndex.maxLength, NAMESPACE_LIMITS.vectorizeIndex.label);
|
|
113
267
|
}
|
|
114
268
|
//#endregion
|
|
115
|
-
export { FEATURE_RESOURCE_KINDS, featureResourceName, featureSecretEntryName, featureWorkerName };
|
|
269
|
+
export { FEATURE_RESOURCE_KINDS, assertFeatureProject, assertFeatureSlugFits, canonicalIssue, featureMarkerInProjectName, featureResourceName, featureSecretEntryName, featureSlugRoom, featureVectorizeIndexName, featureWorkerName, featureWorkflowName, isFeatureOwnedName, maxFeatureSlug, parseFeatureName, tightestFeatureShape };
|
package/dist/naming/limits.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export type LimitSource = "cloudflare" | "pithy";
|
|
|
28
28
|
* **`truncate`** where Pithy recomputes the name from the same inputs on every command — a D1
|
|
29
29
|
* database, a KV namespace, a bucket, a secret entry, a token. Truncation is deterministic and
|
|
30
30
|
* hash-disambiguated, so provision and teardown still agree, and a failed CI run is avoided.
|
|
31
|
+
*
|
|
32
|
+
* **A feature's names are refused in every namespace, whatever its policy here (#643).** A fitted slug is a
|
|
33
|
+
* short hash, and a hash is a slug another branch can have whole; see `composeFeatureName` in `./feature`.
|
|
31
34
|
*/
|
|
32
35
|
export type OverflowPolicy = "refuse" | "truncate";
|
|
33
36
|
/** One namespace's rule: how long a name may be, and what happens when it would be longer. */
|
|
@@ -180,11 +183,24 @@ export declare const MAX_ISSUE_DIGITS = 6;
|
|
|
180
183
|
/**
|
|
181
184
|
* The slug length a feature resource name keeps verbatim at the worst legal project name.
|
|
182
185
|
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
+
* Eleven — it was twelve until {@link FEATURE_TAIL_SEPARATOR} took a character of it (#643). About where a
|
|
187
|
+
* feature slug stops being readable in a listing. A slug past a feature's budget is refused, not fitted (#643),
|
|
188
|
+
* so this is the room the project-name cap guarantees every project's features at the worst legal input.
|
|
189
|
+
*/
|
|
190
|
+
export declare const MIN_LEGIBLE_SLUG = 11;
|
|
191
|
+
/**
|
|
192
|
+
* **What stands between a feature's slug and whatever it names: two hyphens (#643).**
|
|
193
|
+
*
|
|
194
|
+
* `<project>-f<issue>-<slug>--<thing>`. Nothing else Pithy composes can contain it: every other segment is
|
|
195
|
+
* kebabbed, and a kebab never holds two hyphens in a row. So a name holding it is a feature's and a name without
|
|
196
|
+
* it is not — `acme`'s `staging` can never compose a feature's name, whatever its project or environment is
|
|
197
|
+
* called — and it ends the slug unambiguously, so `feature/12-x` binding `PROD_DB` and `feature/12-x-prod`
|
|
198
|
+
* binding `DB` no longer compose one database between them.
|
|
199
|
+
*
|
|
200
|
+
* It cost the slug a character: {@link MIN_LEGIBLE_SLUG} gave one up so {@link FEATURE_DERIVED_PROJECT_NAME},
|
|
201
|
+
* and with it every project name already accepted, stays where it was.
|
|
186
202
|
*/
|
|
187
|
-
export declare const
|
|
203
|
+
export declare const FEATURE_TAIL_SEPARATOR = "--";
|
|
188
204
|
/**
|
|
189
205
|
* The binding length a feature resource name keeps verbatim at the worst legal project name.
|
|
190
206
|
*
|
|
@@ -212,7 +228,7 @@ export declare const MAX_FEATURE_KIND = 2;
|
|
|
212
228
|
*/
|
|
213
229
|
export declare const WORKFLOW_DERIVED_PROJECT_NAME: number;
|
|
214
230
|
/**
|
|
215
|
-
* What the longest feature resource name leaves a project — `<project>-f<issue>-<slug
|
|
231
|
+
* What the longest feature resource name leaves a project — `<project>-f<issue>-<slug>--<binding>-<kind>`,
|
|
216
232
|
* against R2's 63 (the strictest of the three kinds a feature provisions, and the same number a
|
|
217
233
|
* feature Worker script gets).
|
|
218
234
|
*
|
|
@@ -220,8 +236,8 @@ export declare const WORKFLOW_DERIVED_PROJECT_NAME: number;
|
|
|
220
236
|
* 63 an R2 bucket name (NAMESPACE_LIMITS.r2)
|
|
221
237
|
* - 2 `-f`
|
|
222
238
|
* - 6 the issue number, six digits — `f999999` (MAX_ISSUE_DIGITS)
|
|
223
|
-
* -
|
|
224
|
-
* -
|
|
239
|
+
* - 12 `-` + a legible slug (MIN_LEGIBLE_SLUG)
|
|
240
|
+
* - 14 `--` + a binding kept whole (FEATURE_TAIL_SEPARATOR, MIN_VERBATIM_BINDING)
|
|
225
241
|
* - 3 `-` + the kind, `d1` | `kv` | `r2`
|
|
226
242
|
* ----
|
|
227
243
|
* 26
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/naming/limits.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AAEH,sFAAsF;AACtF,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;AAEjD
|
|
1
|
+
{"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/naming/limits.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;GAcG;AAEH,sFAAsF;AACtF,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;AAEjD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEnD,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kGAAkG;IAClG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAElC;;;GAGG;AACH,eAAO,MAAM,gBAAgB;IAC3B;;;OAGG;;iBACS,KAAK,EAAE,iBAAiB;iBAAE,SAAS,EAAE,EAAE;iBAAE,SAAS,EAAE,CAAC;iBAAE,MAAM,EAAE,QAAQ;iBAAE,MAAM,EAAE,YAAY;;IACzG;;;;;;OAMG;;iBACO,KAAK,EAAE,sBAAsB;iBAAE,SAAS,EAAE,EAAE;iBAAE,SAAS,EAAE,CAAC;iBAAE,MAAM,EAAE,QAAQ;iBAAE,MAAM,EAAE,YAAY;;IAC5G;;;OAGG;;iBACG,KAAK,EAAE,mBAAmB;iBAAE,SAAS,EAAE,EAAE;iBAAE,SAAS,EAAE,CAAC;iBAAE,MAAM,EAAE,UAAU;iBAAE,MAAM,EAAE,YAAY;;IACvG;;;;OAIG;;iBAED,KAAK,EAAE,wBAAwB;iBAC/B,SAAS,EAAE,EAAE;iBACb,SAAS,EAAE,CAAC;iBACZ,MAAM,EAAE,QAAQ;iBAChB,MAAM,EAAE,YAAY;;IAEtB;;;;;OAKG;;iBACG,KAAK,EAAE,sBAAsB;iBAAE,SAAS,EAAE,GAAG;iBAAE,SAAS,EAAE,CAAC;iBAAE,MAAM,EAAE,UAAU;iBAAE,MAAM,EAAE,YAAY;;IAC3G;;;OAGG;;iBACG,KAAK,EAAE,oBAAoB;iBAAE,SAAS;iBAAkB,SAAS,EAAE,CAAC;iBAAE,MAAM,EAAE,UAAU;iBAAE,MAAM,EAAE,OAAO;;IAC/G;;;OAGG;;iBAED,KAAK,EAAE,4BAA4B;iBACnC,SAAS;iBACT,SAAS,EAAE,CAAC;iBACZ,MAAM,EAAE,UAAU;iBAClB,MAAM,EAAE,OAAO;;IAEjB;;;OAGG;;iBAED,KAAK,EAAE,6BAA6B;iBACpC,SAAS;iBACT,SAAS,EAAE,CAAC;iBACZ,MAAM,EAAE,UAAU;iBAClB,MAAM,EAAE,OAAO;;CAEgC,CAAC;AAEpD,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,gBAAgB,CAAC;AAEtD;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,QAAkC,CAAC;AAElE;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,6BAA6B,QAC+C,CAAC;AAE1F;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,QAMjB,CAAC"}
|
package/dist/naming/limits.js
CHANGED
|
@@ -137,11 +137,24 @@ const MAX_ISSUE_DIGITS = 6;
|
|
|
137
137
|
/**
|
|
138
138
|
* The slug length a feature resource name keeps verbatim at the worst legal project name.
|
|
139
139
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
140
|
+
* Eleven — it was twelve until {@link FEATURE_TAIL_SEPARATOR} took a character of it (#643). About where a
|
|
141
|
+
* feature slug stops being readable in a listing. A slug past a feature's budget is refused, not fitted (#643),
|
|
142
|
+
* so this is the room the project-name cap guarantees every project's features at the worst legal input.
|
|
143
143
|
*/
|
|
144
|
-
const MIN_LEGIBLE_SLUG =
|
|
144
|
+
const MIN_LEGIBLE_SLUG = 11;
|
|
145
|
+
/**
|
|
146
|
+
* **What stands between a feature's slug and whatever it names: two hyphens (#643).**
|
|
147
|
+
*
|
|
148
|
+
* `<project>-f<issue>-<slug>--<thing>`. Nothing else Pithy composes can contain it: every other segment is
|
|
149
|
+
* kebabbed, and a kebab never holds two hyphens in a row. So a name holding it is a feature's and a name without
|
|
150
|
+
* it is not — `acme`'s `staging` can never compose a feature's name, whatever its project or environment is
|
|
151
|
+
* called — and it ends the slug unambiguously, so `feature/12-x` binding `PROD_DB` and `feature/12-x-prod`
|
|
152
|
+
* binding `DB` no longer compose one database between them.
|
|
153
|
+
*
|
|
154
|
+
* It cost the slug a character: {@link MIN_LEGIBLE_SLUG} gave one up so {@link FEATURE_DERIVED_PROJECT_NAME},
|
|
155
|
+
* and with it every project name already accepted, stays where it was.
|
|
156
|
+
*/
|
|
157
|
+
const FEATURE_TAIL_SEPARATOR = "--";
|
|
145
158
|
/**
|
|
146
159
|
* The binding length a feature resource name keeps verbatim at the worst legal project name.
|
|
147
160
|
*
|
|
@@ -169,7 +182,7 @@ const MAX_FEATURE_KIND = 2;
|
|
|
169
182
|
*/
|
|
170
183
|
const WORKFLOW_DERIVED_PROJECT_NAME = NAMESPACE_LIMITS.workflow.maxLength - 1 - MAX_ENVIRONMENT_NAME - 1 - 22;
|
|
171
184
|
/**
|
|
172
|
-
* What the longest feature resource name leaves a project — `<project>-f<issue>-<slug
|
|
185
|
+
* What the longest feature resource name leaves a project — `<project>-f<issue>-<slug>--<binding>-<kind>`,
|
|
173
186
|
* against R2's 63 (the strictest of the three kinds a feature provisions, and the same number a
|
|
174
187
|
* feature Worker script gets).
|
|
175
188
|
*
|
|
@@ -177,13 +190,13 @@ const WORKFLOW_DERIVED_PROJECT_NAME = NAMESPACE_LIMITS.workflow.maxLength - 1 -
|
|
|
177
190
|
* 63 an R2 bucket name (NAMESPACE_LIMITS.r2)
|
|
178
191
|
* - 2 `-f`
|
|
179
192
|
* - 6 the issue number, six digits — `f999999` (MAX_ISSUE_DIGITS)
|
|
180
|
-
* -
|
|
181
|
-
* -
|
|
193
|
+
* - 12 `-` + a legible slug (MIN_LEGIBLE_SLUG)
|
|
194
|
+
* - 14 `--` + a binding kept whole (FEATURE_TAIL_SEPARATOR, MIN_VERBATIM_BINDING)
|
|
182
195
|
* - 3 `-` + the kind, `d1` | `kv` | `r2`
|
|
183
196
|
* ----
|
|
184
197
|
* 26
|
|
185
198
|
* ```
|
|
186
199
|
*/
|
|
187
|
-
const FEATURE_DERIVED_PROJECT_NAME = NAMESPACE_LIMITS.r2.maxLength - 2 - 6 -
|
|
200
|
+
const FEATURE_DERIVED_PROJECT_NAME = NAMESPACE_LIMITS.r2.maxLength - 2 - 6 - 12 - 14 - 3;
|
|
188
201
|
//#endregion
|
|
189
|
-
export { FEATURE_DERIVED_PROJECT_NAME, MAX_CAPABILITY_JOB, MAX_FEATURE_KIND, MAX_ISSUE_DIGITS, MAX_PITHY_NAME, MIN_LEGIBLE_SLUG, MIN_VERBATIM_BINDING, NAMESPACE_LIMITS, WORKFLOW_DERIVED_PROJECT_NAME };
|
|
202
|
+
export { FEATURE_DERIVED_PROJECT_NAME, FEATURE_TAIL_SEPARATOR, MAX_CAPABILITY_JOB, MAX_FEATURE_KIND, MAX_ISSUE_DIGITS, MAX_PITHY_NAME, MIN_LEGIBLE_SLUG, MIN_VERBATIM_BINDING, NAMESPACE_LIMITS, WORKFLOW_DERIVED_PROJECT_NAME };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type { WorkflowHostNameParts } from "../workflow/naming";
|
|
3
4
|
import { type FeatureIdentity, type FeatureResourceKind } from "./feature";
|
|
4
5
|
/**
|
|
5
6
|
* **What a provisioning run names its resources, and which `env.<name>` stanza it writes them into —
|
|
@@ -154,11 +155,22 @@ export interface ProvisionScope {
|
|
|
154
155
|
/**
|
|
155
156
|
* This scope's CF Secrets Store entry name for a declared secret.
|
|
156
157
|
*
|
|
157
|
-
*
|
|
158
|
-
* account-level value every environment binds, so
|
|
159
|
-
* `<project>-global-<secret>`.
|
|
158
|
+
* In a declared environment a `global` secret is the one name that does **not** take the scope's segment:
|
|
159
|
+
* it is a single account-level value every declared environment binds, so it resolves to the project's
|
|
160
|
+
* `<project>-global-<secret>`. A feature is the exception — it shares nothing with any other environment,
|
|
161
|
+
* so its entry for a `global` secret is its own (#643). See {@link featureScope}.
|
|
160
162
|
*/
|
|
161
163
|
secretEntry(secret: string, secretScope: SecretNameScope): string;
|
|
164
|
+
/**
|
|
165
|
+
* What a capability's host Worker and its Workflows are named from in this scope — handed, with the
|
|
166
|
+
* capability, to `workflowHostName` and `workflowScriptName`. It is what an app Worker's cross-script
|
|
167
|
+
* `workflows` entry and the host's own config are both composed from (#643).
|
|
168
|
+
*
|
|
169
|
+
* On the scope for the reason every other name is: a declared environment composes
|
|
170
|
+
* `<project>-<env>-<capability>[-<job>]`, and a feature composes `<project>-f<issue>-<slug>-…`, because
|
|
171
|
+
* `<project>-feature-email-send` would be one Workflow every open branch deployed over every other's.
|
|
172
|
+
*/
|
|
173
|
+
readonly workflowHost: Omit<WorkflowHostNameParts, "capability">;
|
|
162
174
|
}
|
|
163
175
|
/**
|
|
164
176
|
* The same branch, one line down, for a provisioned resource: a `global` binding resolves to the
|
|
@@ -240,14 +252,16 @@ export declare function environmentScope(project: string, environment: string):
|
|
|
240
252
|
* Its stanza is {@link FEATURE_ENVIRONMENT} and nothing else, which is what makes a feature's resources
|
|
241
253
|
* in a declared environment's stanza unexpressible rather than merely discouraged.
|
|
242
254
|
*
|
|
243
|
-
* **A feature takes a binding's {@link BindingNaming} and ignores it
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
255
|
+
* **A feature takes a binding's {@link BindingNaming} and ignores it, and a secret's `global` scope with it.**
|
|
256
|
+
* A global *database* is a resource a feature would **migrate** — `provisionFeature` runs `pithy migrate`
|
|
257
|
+
* against everything it names — so honoring `global` would point one branch's schema changes at the project's
|
|
258
|
+
* live suppression list, and point teardown at it afterwards. A global *secret* went the other way until #643:
|
|
259
|
+
* a feature bound the project's copy. It no longer does. **Nothing is shared between a feature and any other
|
|
260
|
+
* feature, staging or prod** — the account and its one Secrets Store are the only containers that cannot be
|
|
261
|
+
* split, and every entry inside the store is split. So a feature names its own entry for a `global` secret
|
|
262
|
+
* too: a mintable one is minted for it, and a supplied one is reported missing under the feature's name rather
|
|
263
|
+
* than bound to the value every environment reads. A feature owns everything it names, exactly so that
|
|
264
|
+
* destroying the feature destroys it all; there is no shared name to match and nothing for teardown to spare.
|
|
251
265
|
*
|
|
252
266
|
* `resource` is ignored for the same reason it is moot: a feature's `<thing>` segment is the binding plus
|
|
253
267
|
* the kind, and the whole name is recomputed on teardown rather than looked up, so nothing outside the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provisionScope.d.ts","sourceRoot":"","sources":["../../src/naming/provisionScope.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"provisionScope.d.ts","sourceRoot":"","sources":["../../src/naming/provisionScope.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAIzB,MAAM,WAAW,CAAC;AAInB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,QAAQ,CAAC;AAEvD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAoB,SAAQ,aAAa;IACxD,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;CAC1B;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,oBAAoB;IACnC,2GAA2G;IAC3G,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B,6FAA6F;IAC7F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAAC;IACpF;;;;;;;;;OASG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChE;;;;;;;OAOG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,eAAe,GAAG,MAAM,CAAC;IAClE;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,IAAI,CAAC,qBAAqB,EAAE,YAAY,CAAC,CAAC;CAClE;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,mBAAmB,GAC1B,MAAM,CAAC;AACV,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,mBAAmB,EACzB,MAAM,EAAE,aAAa,EACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAChC,MAAM,CAAC;AAsBV;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAKpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc,CA8BrF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,eAAe,GAAG,cAAc,CAiBtE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,oBAAoB,GAAG,MAAM,EAAE,CAY1G"}
|
|
@@ -97,7 +97,11 @@ function environmentScope(project, environment) {
|
|
|
97
97
|
});
|
|
98
98
|
return name;
|
|
99
99
|
},
|
|
100
|
-
secretEntry: (secret, secretScope) => secretEntryName(project, secret, secretScope, () => names.secretEntry(secret))
|
|
100
|
+
secretEntry: (secret, secretScope) => secretEntryName(project, secret, secretScope, () => names.secretEntry(secret)),
|
|
101
|
+
workflowHost: {
|
|
102
|
+
project,
|
|
103
|
+
env: environment
|
|
104
|
+
}
|
|
101
105
|
};
|
|
102
106
|
}
|
|
103
107
|
/**
|
|
@@ -107,14 +111,16 @@ function environmentScope(project, environment) {
|
|
|
107
111
|
* Its stanza is {@link FEATURE_ENVIRONMENT} and nothing else, which is what makes a feature's resources
|
|
108
112
|
* in a declared environment's stanza unexpressible rather than merely discouraged.
|
|
109
113
|
*
|
|
110
|
-
* **A feature takes a binding's {@link BindingNaming} and ignores it
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
114
|
+
* **A feature takes a binding's {@link BindingNaming} and ignores it, and a secret's `global` scope with it.**
|
|
115
|
+
* A global *database* is a resource a feature would **migrate** — `provisionFeature` runs `pithy migrate`
|
|
116
|
+
* against everything it names — so honoring `global` would point one branch's schema changes at the project's
|
|
117
|
+
* live suppression list, and point teardown at it afterwards. A global *secret* went the other way until #643:
|
|
118
|
+
* a feature bound the project's copy. It no longer does. **Nothing is shared between a feature and any other
|
|
119
|
+
* feature, staging or prod** — the account and its one Secrets Store are the only containers that cannot be
|
|
120
|
+
* split, and every entry inside the store is split. So a feature names its own entry for a `global` secret
|
|
121
|
+
* too: a mintable one is minted for it, and a supplied one is reported missing under the feature's name rather
|
|
122
|
+
* than bound to the value every environment reads. A feature owns everything it names, exactly so that
|
|
123
|
+
* destroying the feature destroys it all; there is no shared name to match and nothing for teardown to spare.
|
|
118
124
|
*
|
|
119
125
|
* `resource` is ignored for the same reason it is moot: a feature's `<thing>` segment is the binding plus
|
|
120
126
|
* the kind, and the whole name is recomputed on teardown rather than looked up, so nothing outside the
|
|
@@ -133,7 +139,12 @@ function featureScope(identity) {
|
|
|
133
139
|
honorsGlobal: false,
|
|
134
140
|
resource: (binding, kind) => featureResourceName(identity, binding, kind),
|
|
135
141
|
worker: ({ app }) => featureWorkerName(identity, app),
|
|
136
|
-
secretEntry: (secret
|
|
142
|
+
secretEntry: (secret) => featureSecretEntryName(identity, secret),
|
|
143
|
+
workflowHost: {
|
|
144
|
+
project: identity.project,
|
|
145
|
+
env: FEATURE_ENVIRONMENT,
|
|
146
|
+
feature: identity
|
|
147
|
+
}
|
|
137
148
|
};
|
|
138
149
|
}
|
|
139
150
|
/**
|
|
@@ -160,7 +171,13 @@ function featureScope(identity) {
|
|
|
160
171
|
*/
|
|
161
172
|
function featureWorkerScriptNames(identity, worker) {
|
|
162
173
|
const current = featureScope(identity).worker(worker);
|
|
163
|
-
|
|
174
|
+
let beforeSingleProject;
|
|
175
|
+
try {
|
|
176
|
+
beforeSingleProject = featureWorkerName(identity, worker.script);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof ValidationError) return [current];
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
164
181
|
return current === beforeSingleProject ? [current] : [current, beforeSingleProject];
|
|
165
182
|
}
|
|
166
183
|
//#endregion
|
package/dist/seed/seed.d.ts
CHANGED
|
@@ -145,8 +145,10 @@ export interface SeedPrepareContext {
|
|
|
145
145
|
/** The project name (the root `pithy.config.ts` `name`), for messages and per-project lookups. */
|
|
146
146
|
project: string;
|
|
147
147
|
/**
|
|
148
|
-
* Where this Worker answers
|
|
149
|
-
*
|
|
148
|
+
* Where this Worker answers — in `dev`, locally, `http://localhost:8807` and the like; off `dev`, its own
|
|
149
|
+
* address (#643): a declared environment's declared one, a feature's `https://<script>.<subdomain>.workers.dev`.
|
|
150
|
+
* `pithy seed --host` overrides it. `null` in a checkout that was never allocated a port block, and for a
|
|
151
|
+
* deployed Worker with no address at all.
|
|
150
152
|
*
|
|
151
153
|
* The one address a fixture cannot write down. A dev port is *allocated*, not configured: a checkout
|
|
152
154
|
* reserves a block and pins one port per Worker into `.dev.config.json`, so `http://localhost:8787` is
|
|
@@ -163,17 +165,44 @@ export interface SeedPrepareContext {
|
|
|
163
165
|
* moves. Identity must not.
|
|
164
166
|
*
|
|
165
167
|
* **`null` is an answer, never a guess.** A plain clone that has never run `pithy dev`, a Worker added
|
|
166
|
-
* after the block was pinned, or
|
|
167
|
-
*
|
|
168
|
-
*
|
|
168
|
+
* after the block was pinned, or a deployed Worker that declares no address and has none to derive. An
|
|
169
|
+
* invented origin would be indistinguishable from a real one, so a set that cannot work without one
|
|
170
|
+
* refuses and says so.
|
|
169
171
|
*/
|
|
170
172
|
origin: string | null;
|
|
171
173
|
/**
|
|
172
174
|
* Read one of this environment's secrets by name, or `undefined` when it is not set. Local dev resolves
|
|
173
|
-
* every secret from
|
|
174
|
-
* operator's disk, and a set that needs one must
|
|
175
|
+
* every secret from the dev secrets file, so this answers there; a deployed environment's secrets are not
|
|
176
|
+
* on the operator's disk, so there it refuses, and a set that needs one must be `dev`-only. A feature is a
|
|
177
|
+
* deployed environment here too (#643): its secrets exist only in its own Cloudflare stores, which the CLI
|
|
178
|
+
* can write and never read.
|
|
175
179
|
*/
|
|
176
180
|
secret: (name: string) => Promise<string | undefined>;
|
|
181
|
+
/**
|
|
182
|
+
* The value **this run just minted** for a secret, or `undefined` — never what the environment holds.
|
|
183
|
+
*
|
|
184
|
+
* `pithy provision` creates a random value for every `cf-secrets-store` secret the registry declares
|
|
185
|
+
* mintable, writes it into the account's Secrets Store, and lets it go. The Secrets Store is write-only
|
|
186
|
+
* from the CLI, so that write is the only moment the value exists anywhere the toolchain can see it —
|
|
187
|
+
* and provisioning migrates and seeds in the same process, a few steps later. A fixture that must
|
|
188
|
+
* *seal* something at creation time (encrypt a private key under the environment's key-encryption
|
|
189
|
+
* secret, derive an id that the running app will re-derive) can only do it then. Afterwards the value
|
|
190
|
+
* is unrecoverable by design.
|
|
191
|
+
*
|
|
192
|
+
* **This is not {@link secret}, and the two are not interchangeable.** `secret` answers what the
|
|
193
|
+
* environment holds and refuses outside `dev`, absolutely (#159); that rule is untouched. This answers
|
|
194
|
+
* the strictly narrower question *did this run create one, a moment ago, in memory*. A set asking here
|
|
195
|
+
* for a secret this run did not mint gets `undefined` wherever it runs, in `dev` or anywhere else.
|
|
196
|
+
*
|
|
197
|
+
* **Synchronous, because it cannot fetch.** There is no file to open and no request to make; a promise
|
|
198
|
+
* in this signature would suggest otherwise.
|
|
199
|
+
*
|
|
200
|
+
* **Empty is ordinary.** A re-run mints nothing — absence is checked before anything is generated — and
|
|
201
|
+
* a standalone `pithy seed` mints nothing at all. Both answer `undefined` for every name, and neither
|
|
202
|
+
* is an error. A set that needs a minted value and is handed none says so itself; nothing is invented
|
|
203
|
+
* on its behalf.
|
|
204
|
+
*/
|
|
205
|
+
mintedThisRun: (name: string) => string | undefined;
|
|
177
206
|
/**
|
|
178
207
|
* The developer's machine-local preferences for this project, read from the Pithy config directory
|
|
179
208
|
* (`~/.config/pithy/<project>/dev.json`, or `%APPDATA%\pithy\<project>\dev.json` on Windows), parsed but
|