@effected/github-actions 0.7.0 → 0.9.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/ActionEnvironment.js +24 -1
- package/CheckDocument.js +128 -13
- package/ManagedDocument.js +61 -18
- package/README.md +34 -6
- package/index.d.ts +182 -32
- package/index.js +2 -2
- package/package.json +9 -9
package/ActionEnvironment.js
CHANGED
|
@@ -286,6 +286,26 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
|
|
|
286
286
|
* `undefined` means *not served*, so a suite cannot arrange `undefined` as
|
|
287
287
|
* the payload itself. A webhook payload is always a JSON object; the case
|
|
288
288
|
* does not arise, and reserving the sentinel keeps the argument optional.
|
|
289
|
+
*
|
|
290
|
+
* When a suite wants the **real read path** rather than a served value —
|
|
291
|
+
* exercising the actual `GITHUB_EVENT_PATH` read-and-parse — this member is
|
|
292
|
+
* the seam: it leaves `FileSystem` in `R` precisely so the suite chooses
|
|
293
|
+
* the filesystem. Compose it with `@effected/memfs`'s seeded in-memory
|
|
294
|
+
* volume rather than a hand-rolled stub: the volume honors the whole
|
|
295
|
+
* `FileSystem` contract, and an unseeded path fails typed `NotFound`
|
|
296
|
+
* instead of whatever a hand stub happens to answer.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* import { ActionEnvironment } from "@effected/github-actions";
|
|
301
|
+
* import { MemoryFileSystem } from "@effected/memfs";
|
|
302
|
+
* import { Layer } from "effect";
|
|
303
|
+
*
|
|
304
|
+
* const layer = Layer.effect(
|
|
305
|
+
* ActionEnvironment,
|
|
306
|
+
* ActionEnvironment.makeTest({ GITHUB_EVENT_PATH: "/event.json" }),
|
|
307
|
+
* ).pipe(Layer.provide(MemoryFileSystem.layerWith({ "/event.json": JSON.stringify({ action: "opened" }) })));
|
|
308
|
+
* ```
|
|
289
309
|
*/
|
|
290
310
|
static makeTest = (overrides = {}, payload) => Effect.map(make({
|
|
291
311
|
...TEST_DEFAULTS,
|
|
@@ -302,7 +322,10 @@ var ActionEnvironment = class ActionEnvironment extends Context.Service()("@effe
|
|
|
302
322
|
* The stub is why the payload has to be served here rather than through the
|
|
303
323
|
* filesystem: `make` captures the filesystem at construction, so seeding
|
|
304
324
|
* `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
|
|
305
|
-
* filesystem and fail. Pass the payload as the second argument instead
|
|
325
|
+
* filesystem and fail. Pass the payload as the second argument instead —
|
|
326
|
+
* or, for a suite that wants the real read path over a full filesystem,
|
|
327
|
+
* compose {@link ActionEnvironment.makeTest} with `@effected/memfs`'s
|
|
328
|
+
* seeded volume; the recipe is on that member.
|
|
306
329
|
*
|
|
307
330
|
* @example
|
|
308
331
|
* ```ts
|
package/CheckDocument.js
CHANGED
|
@@ -29,29 +29,112 @@ var CheckReport = class extends Schema.Class("CheckReport")({
|
|
|
29
29
|
url: Schema.optionalKey(Schema.String)
|
|
30
30
|
}) {};
|
|
31
31
|
/**
|
|
32
|
-
* Raised when the check document cannot be regenerated or written.
|
|
32
|
+
* Raised when the check document cannot be regenerated, read back or written.
|
|
33
33
|
*
|
|
34
34
|
* @remarks
|
|
35
35
|
* `render` — projecting the registry onto the document failed, almost always
|
|
36
36
|
* because a reported `detail` contains a line the region scanner would read
|
|
37
37
|
* as a marker; the underlying `ManagedDocumentError` rides in `cause`.
|
|
38
|
-
* `
|
|
39
|
-
*
|
|
40
|
-
*
|
|
38
|
+
* `read` — the sink's read-back of the live document failed; its error rides
|
|
39
|
+
* in `cause`. `sink` — the consumer's write effect failed; its error rides in
|
|
40
|
+
* `cause`. Callers of `flush` branch on `kind`; the background reconciler
|
|
41
|
+
* never fails a fiber with this — it logs and retries on the next report.
|
|
41
42
|
*
|
|
42
43
|
* @public
|
|
43
44
|
*/
|
|
44
45
|
var CheckDocumentError = class extends Schema.TaggedError()("CheckDocumentError", {
|
|
45
|
-
/** Which
|
|
46
|
-
kind: Schema.Literals([
|
|
46
|
+
/** Which phase failed: regenerating the document, reading it back, or writing it. */
|
|
47
|
+
kind: Schema.Literals([
|
|
48
|
+
"render",
|
|
49
|
+
"read",
|
|
50
|
+
"sink"
|
|
51
|
+
]),
|
|
47
52
|
/** The underlying failure. */
|
|
48
53
|
cause: Schema.optionalKey(Schema.Defect())
|
|
49
54
|
}) {
|
|
50
55
|
get message() {
|
|
51
|
-
return this.kind === "render" ? "The check document could not be regenerated from the reported state" : "Writing the check document failed";
|
|
56
|
+
return this.kind === "render" ? "The check document could not be regenerated from the reported state" : this.kind === "read" ? "Reading the check document's current text failed" : "Writing the check document failed";
|
|
52
57
|
}
|
|
53
58
|
};
|
|
54
59
|
/**
|
|
60
|
+
* A run's identity for staleness ordering: when it started and which run it
|
|
61
|
+
* is.
|
|
62
|
+
*
|
|
63
|
+
* @remarks
|
|
64
|
+
* Two delayed workflows can race for one document — a re-run's stale pass
|
|
65
|
+
* must not overwrite a newer run's regions. The ordering is **total**:
|
|
66
|
+
* `at` compares first (as epoch milliseconds when both sides `Date.parse`
|
|
67
|
+
* cleanly, lexically otherwise), `runId` breaks ties (numerically when both
|
|
68
|
+
* parse to finite numbers, lexically otherwise), and two equal stamps compare
|
|
69
|
+
* as at-least-as-recent — a run may refine its own regions.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
var CheckDocumentStamp = class extends Schema.Class("CheckDocumentStamp")({
|
|
74
|
+
/** When the run started — ideally an ISO-8601 instant. */
|
|
75
|
+
at: Schema.String,
|
|
76
|
+
/** The run's identifier — on GitHub, `GITHUB_RUN_ID`. */
|
|
77
|
+
runId: Schema.String
|
|
78
|
+
}) {
|
|
79
|
+
/**
|
|
80
|
+
* Is `incoming` allowed to overwrite regions stamped `existing`?
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* Total and reflexive: equal stamps answer `true`, so a run always may
|
|
84
|
+
* rewrite what it wrote itself. Only a strictly older `incoming` answers
|
|
85
|
+
* `false`.
|
|
86
|
+
*/
|
|
87
|
+
static isAtLeastAsRecent(incoming, existing) {
|
|
88
|
+
const at = compareAt(incoming.at, existing.at);
|
|
89
|
+
if (at !== 0) return at > 0;
|
|
90
|
+
return compareRunId(incoming.runId, existing.runId) >= 0;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/** Epoch order when both sides parse as dates, lexical otherwise. */
|
|
94
|
+
const compareAt = (left, right) => {
|
|
95
|
+
const leftEpoch = Date.parse(left);
|
|
96
|
+
const rightEpoch = Date.parse(right);
|
|
97
|
+
if (!Number.isNaN(leftEpoch) && !Number.isNaN(rightEpoch)) return leftEpoch === rightEpoch ? 0 : leftEpoch < rightEpoch ? -1 : 1;
|
|
98
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Numeric order when both sides are non-blank finite numbers, lexical
|
|
102
|
+
* otherwise.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The blank guard exists because `Number("")` and `Number(" ")` are both a
|
|
106
|
+
* finite `0` — without it a blank `runId` (plausible when `GITHUB_RUN_ID` is
|
|
107
|
+
* unset in a local or self-hosted run) would compare numerically equal to
|
|
108
|
+
* `"0"` and outrank `"-1"`. Blank sides fall to the lexical branch, which
|
|
109
|
+
* stays total and deterministic.
|
|
110
|
+
*/
|
|
111
|
+
const compareRunId = (left, right) => {
|
|
112
|
+
if (left.trim() !== "" && right.trim() !== "") {
|
|
113
|
+
const leftNumber = Number(left);
|
|
114
|
+
const rightNumber = Number(right);
|
|
115
|
+
if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) return leftNumber === rightNumber ? 0 : leftNumber < rightNumber ? -1 : 1;
|
|
116
|
+
}
|
|
117
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
118
|
+
};
|
|
119
|
+
/** The stamp a document's own regions carry, when they carry one. */
|
|
120
|
+
const stampOf = (meta) => {
|
|
121
|
+
const at = meta.at;
|
|
122
|
+
const runId = meta.runId;
|
|
123
|
+
return at === void 0 || runId === void 0 ? void 0 : {
|
|
124
|
+
at,
|
|
125
|
+
runId
|
|
126
|
+
};
|
|
127
|
+
};
|
|
128
|
+
/** The most recent stamp among a document's own regions, if any carry one. */
|
|
129
|
+
const maxStampOf = (regions) => {
|
|
130
|
+
let max;
|
|
131
|
+
for (const region of regions) {
|
|
132
|
+
const stamp = stampOf(region.meta);
|
|
133
|
+
if (stamp !== void 0 && (max === void 0 || CheckDocumentStamp.isAtLeastAsRecent(stamp, max))) max = stamp;
|
|
134
|
+
}
|
|
135
|
+
return max;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
55
138
|
* A living document of check state: an in-process registry the run reports
|
|
56
139
|
* into, debounced onto a marker-delimited document, written through a narrow
|
|
57
140
|
* sink.
|
|
@@ -121,26 +204,58 @@ var CheckDocument = class CheckDocument extends Context.Service()("@effected/git
|
|
|
121
204
|
const versionRef = yield* Ref.make(0);
|
|
122
205
|
const signal = yield* Latch.make(false);
|
|
123
206
|
const gate = yield* Semaphore.make(1);
|
|
207
|
+
const passTimeout = options.sinkTimeout ?? "30 seconds";
|
|
208
|
+
const sink = typeof options.sink === "function" ? { write: options.sink } : options.sink;
|
|
209
|
+
const stamp = options.stamp;
|
|
210
|
+
const stampMeta = stamp === void 0 ? void 0 : {
|
|
211
|
+
at: stamp.at,
|
|
212
|
+
runId: stamp.runId
|
|
213
|
+
};
|
|
214
|
+
const staleLoggedRef = yield* Ref.make(false);
|
|
124
215
|
const reconcile = Effect.fn("CheckDocument.reconcile")(function* () {
|
|
125
216
|
const checks = yield* Ref.get(checksRef);
|
|
126
|
-
const current = yield* Ref.get(writtenRef)
|
|
127
|
-
|
|
217
|
+
const current = sink.read === void 0 ? yield* Ref.get(writtenRef) : (yield* sink.read.pipe(Effect.timeout(passTimeout), Effect.mapError((cause) => new CheckDocumentError({
|
|
218
|
+
kind: "read",
|
|
219
|
+
cause
|
|
220
|
+
})))) ?? "";
|
|
221
|
+
const document = yield* ManagedDocument.parse({
|
|
128
222
|
namespace: options.namespace,
|
|
129
223
|
key: options.key,
|
|
130
224
|
text: current
|
|
131
225
|
}).pipe(Effect.mapError((cause) => new CheckDocumentError({
|
|
132
226
|
kind: "render",
|
|
133
227
|
cause
|
|
134
|
-
})))
|
|
228
|
+
})));
|
|
229
|
+
if (stamp !== void 0) {
|
|
230
|
+
const existing = maxStampOf(document.regions);
|
|
231
|
+
if (existing !== void 0 && !CheckDocumentStamp.isAtLeastAsRecent(stamp, existing)) {
|
|
232
|
+
yield* Effect.annotateCurrentSpan("outcome", "stale");
|
|
233
|
+
yield* ((yield* Ref.getAndSet(staleLoggedRef, true)) ? Effect.logDebug : Effect.logInfo)("CheckDocument pass dropped: the document carries a more recent run's regions", {
|
|
234
|
+
incomingAt: stamp.at,
|
|
235
|
+
incomingRunId: stamp.runId,
|
|
236
|
+
existingAt: existing.at,
|
|
237
|
+
existingRunId: existing.runId
|
|
238
|
+
});
|
|
239
|
+
return "stale";
|
|
240
|
+
}
|
|
241
|
+
yield* Ref.set(staleLoggedRef, false);
|
|
242
|
+
}
|
|
243
|
+
const entries = options.render(checks);
|
|
244
|
+
const next = yield* document.withRegions(stampMeta === void 0 ? entries : entries.map(([key, content]) => [
|
|
245
|
+
key,
|
|
246
|
+
content,
|
|
247
|
+
stampMeta
|
|
248
|
+
])).pipe(Effect.mapError((cause) => new CheckDocumentError({
|
|
135
249
|
kind: "render",
|
|
136
250
|
cause
|
|
137
251
|
})));
|
|
138
|
-
if (next.text === current) return;
|
|
139
|
-
yield*
|
|
252
|
+
if (next.text === current) return "unchanged";
|
|
253
|
+
yield* sink.write(next.text).pipe(Effect.timeout(passTimeout), Effect.mapError((cause) => new CheckDocumentError({
|
|
140
254
|
kind: "sink",
|
|
141
255
|
cause
|
|
142
256
|
})));
|
|
143
257
|
yield* Ref.set(writtenRef, next.text);
|
|
258
|
+
return "written";
|
|
144
259
|
});
|
|
145
260
|
const flush = gate.withPermits(1)(reconcile());
|
|
146
261
|
const daemon = Effect.gen(function* () {
|
|
@@ -190,4 +305,4 @@ const unstubbed = (member) => {
|
|
|
190
305
|
};
|
|
191
306
|
|
|
192
307
|
//#endregion
|
|
193
|
-
export { CheckDocument, CheckDocumentError, CheckReport };
|
|
308
|
+
export { CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport };
|
package/ManagedDocument.js
CHANGED
|
@@ -31,10 +31,14 @@ const REGION_DIALECT = SectionDialect.make({
|
|
|
31
31
|
* The four structural kinds (`unterminatedRegion`, `orphanedEnd`,
|
|
32
32
|
* `overlappingRegions`, `duplicateRegion`) surface at parse time and name an
|
|
33
33
|
* ambiguity whose silent resolution would corrupt content a human wrote by
|
|
34
|
-
* hand. The
|
|
34
|
+
* hand. The three declaration kinds surface when regions are applied:
|
|
35
35
|
* `markerInContent` refuses region content that itself contains a line the
|
|
36
|
-
* scanner would read as a region marker,
|
|
37
|
-
* one call that states two intentions for the same region
|
|
36
|
+
* scanner would read as a region marker, `duplicateDeclaration` refuses
|
|
37
|
+
* one call that states two intentions for the same region, and
|
|
38
|
+
* `invalidAttribute` refuses region metadata whose name is outside the
|
|
39
|
+
* `[A-Za-z][A-Za-z0-9_-]*` grammar or whose value contains `"` or a line
|
|
40
|
+
* break — either would write a marker the region scanner could not read
|
|
41
|
+
* back verbatim.
|
|
38
42
|
*
|
|
39
43
|
* @public
|
|
40
44
|
*/
|
|
@@ -46,17 +50,21 @@ var ManagedDocumentError = class extends Schema.TaggedError()("ManagedDocumentEr
|
|
|
46
50
|
"overlappingRegions",
|
|
47
51
|
"duplicateRegion",
|
|
48
52
|
"markerInContent",
|
|
49
|
-
"duplicateDeclaration"
|
|
53
|
+
"duplicateDeclaration",
|
|
54
|
+
"invalidAttribute"
|
|
50
55
|
]),
|
|
51
56
|
/** 1-based line of the offending marker, for the structural kinds. */
|
|
52
57
|
line: Schema.optionalKey(Schema.Number),
|
|
53
58
|
/** The region key involved, when the failure names one. */
|
|
54
|
-
key: Schema.optionalKey(Schema.String)
|
|
59
|
+
key: Schema.optionalKey(Schema.String),
|
|
60
|
+
/** The offending metadata attribute's name, for `invalidAttribute`. */
|
|
61
|
+
attribute: Schema.optionalKey(Schema.String)
|
|
55
62
|
}) {
|
|
56
63
|
get message() {
|
|
57
64
|
const which = this.key === void 0 ? "" : ` for region "${this.key}"`;
|
|
65
|
+
const named = this.attribute === void 0 ? "" : ` (attribute "${this.attribute}")`;
|
|
58
66
|
const where = this.line === void 0 ? "" : ` at line ${this.line}`;
|
|
59
|
-
return `${KIND_PROSE[this.kind]}${which}${where}`;
|
|
67
|
+
return `${KIND_PROSE[this.kind]}${which}${named}${where}`;
|
|
60
68
|
}
|
|
61
69
|
};
|
|
62
70
|
const KIND_PROSE = {
|
|
@@ -65,7 +73,8 @@ const KIND_PROSE = {
|
|
|
65
73
|
overlappingRegions: "Managed region opens inside another",
|
|
66
74
|
duplicateRegion: "Managed region appears twice",
|
|
67
75
|
markerInContent: "Region content contains a managed-region marker",
|
|
68
|
-
duplicateDeclaration: "Region was declared twice in one call"
|
|
76
|
+
duplicateDeclaration: "Region was declared twice in one call",
|
|
77
|
+
invalidAttribute: "Region metadata has an invalid attribute name or value"
|
|
69
78
|
};
|
|
70
79
|
/** Map a templates scan failure onto this surface's vocabulary. */
|
|
71
80
|
const STRUCTURAL_KIND = {
|
|
@@ -173,7 +182,29 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
173
182
|
const found = this.ownRegions().find((entry) => entry.key === key);
|
|
174
183
|
return found === void 0 ? Option.none() : Option.some(found.content);
|
|
175
184
|
}
|
|
176
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* One region's content and metadata together, if the document carries it.
|
|
187
|
+
*
|
|
188
|
+
* @remarks
|
|
189
|
+
* `meta` is the region's `name="value"` marker attributes, empty when the
|
|
190
|
+
* marker carries none. Metadata never participates in region
|
|
191
|
+
* addressability — this looks up by key exactly as
|
|
192
|
+
* {@link ManagedDocument.region} does.
|
|
193
|
+
*/
|
|
194
|
+
entry(key) {
|
|
195
|
+
const found = this.ownRegions().find((candidate) => candidate.key === key);
|
|
196
|
+
return found === void 0 ? Option.none() : Option.some({
|
|
197
|
+
content: found.content,
|
|
198
|
+
meta: found.meta
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Every region this document carries, in document order.
|
|
203
|
+
*
|
|
204
|
+
* @remarks
|
|
205
|
+
* `meta` is always present: a region whose marker carries no attributes
|
|
206
|
+
* reports an empty record, so callers never branch on absence.
|
|
207
|
+
*/
|
|
177
208
|
get regions() {
|
|
178
209
|
return this.ownRegions();
|
|
179
210
|
}
|
|
@@ -182,12 +213,22 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
182
213
|
*
|
|
183
214
|
* @remarks
|
|
184
215
|
* The synchronous primitive; {@link ManagedDocument.withRegions} derives
|
|
185
|
-
* from it. Each entry pairs a region key with its full new content
|
|
186
|
-
* region already present is **replaced in place**; a
|
|
187
|
-
* yet is added; regions not named are left exactly as
|
|
188
|
-
* byte outside a managed region survives untouched.
|
|
189
|
-
* change nothing returns byte-identical text, which
|
|
190
|
-
* compare-and-skip instead of issuing a write.
|
|
216
|
+
* from it. Each entry pairs a region key with its full new content, plus
|
|
217
|
+
* optional metadata. A region already present is **replaced in place**; a
|
|
218
|
+
* region not present yet is added; regions not named are left exactly as
|
|
219
|
+
* they are; and every byte outside a managed region survives untouched.
|
|
220
|
+
* Applying entries that change nothing returns byte-identical text, which
|
|
221
|
+
* is what lets a caller compare-and-skip instead of issuing a write.
|
|
222
|
+
*
|
|
223
|
+
* `meta` becomes `name="value"` attributes on the region's BEGIN marker.
|
|
224
|
+
* It round-trips verbatim through {@link ManagedDocument.entry}, survives
|
|
225
|
+
* writes by parties that do not know about it — a region not named in a
|
|
226
|
+
* call keeps its content *and* its metadata untouched — and never
|
|
227
|
+
* participates in region addressability: a region is found by key alone,
|
|
228
|
+
* so changing metadata updates the region in place. A two-element entry
|
|
229
|
+
* behaves exactly as it always has. Names must match
|
|
230
|
+
* `[A-Za-z][A-Za-z0-9_-]*` and values may not contain `"` or a line
|
|
231
|
+
* break; violations fail typed as `invalidAttribute`.
|
|
191
232
|
*
|
|
192
233
|
* Region keys share the name grammar `namespace` and `key` obey; a key
|
|
193
234
|
* outside it is a wiring error and throws at construction rather than
|
|
@@ -203,17 +244,18 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
203
244
|
...failure.key === void 0 ? {} : { key: failure.key }
|
|
204
245
|
}));
|
|
205
246
|
}
|
|
206
|
-
const declared = entries.map(([key, content]) => SectionId.make({
|
|
247
|
+
const declared = entries.map(([key, content, meta]) => SectionId.make({
|
|
207
248
|
key: this.wireKey(key),
|
|
208
249
|
commentStyle: CommentStyle.html
|
|
209
|
-
}).section(content));
|
|
250
|
+
}).section(content, meta));
|
|
210
251
|
const reconciled = parsed.success.reconcile(declared);
|
|
211
252
|
if (Result.isFailure(reconciled)) {
|
|
212
253
|
const failure = reconciled.failure;
|
|
213
254
|
if (failure.reason === "unknownCommentStyle") throw new Error("ManagedDocument: the region dialect rejected its own comment style");
|
|
214
255
|
return Result.fail(new ManagedDocumentError({
|
|
215
256
|
kind: failure.reason,
|
|
216
|
-
key: this.regionKeyOf(failure.key)
|
|
257
|
+
key: this.regionKeyOf(failure.key),
|
|
258
|
+
...failure.attribute === void 0 ? {} : { attribute: failure.attribute }
|
|
217
259
|
}));
|
|
218
260
|
}
|
|
219
261
|
const withSentinel = this.ensureSentinel(reconciled.success.text, parsed.success.eol);
|
|
@@ -259,7 +301,8 @@ var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocumen
|
|
|
259
301
|
const prefix = `${this.namespace}.${this.key}.`;
|
|
260
302
|
return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
|
|
261
303
|
key: placed.section.key.slice(prefix.length),
|
|
262
|
-
content: placed.section.content
|
|
304
|
+
content: placed.section.content,
|
|
305
|
+
meta: placed.section.attributes
|
|
263
306
|
}));
|
|
264
307
|
}
|
|
265
308
|
/**
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
The GitHub Actions runtime for [Effect](https://effect.website) v4: the services an action needs to talk to the runner it is executing inside. `Action.run` composes the runtime, runs your program, renders a failure as an `::error::` annotation and sets the exit code. `ActionInput` reads workflow inputs as typed `Config` values — string, boolean, integer, redacted secret, multiline list, `key=value` pairs, or a schema-decoded JSON blob — with one absence rule shared by every accessor: unset and `""` are both missing data. `ActionOutputs`, `ActionState`, `ActionLogger` and a fiber-local `ActionEnvironment` round out the runner surface, alongside `ActionCache`, `Artifact`, a metadata-carrying `BlobStore` (S3-compatible or the runner's own cache), `OidcTokenIssuer` and `ToolInstaller` for the heavier protocols. A reporting suite — `GitHubMarkdown`, `ManagedDocument`, `CheckState` and `CheckDocument` — covers the other direction: rendering what a run did onto a pull request comment, a check run or the job summary. No `@actions/*` dependency anywhere — the cache, artifact and tool-cache protocols are implemented directly against their HTTP APIs.
|
|
9
9
|
|
|
10
10
|
> **Pre-release.** This package is part of the `@effected/*` kit, in pre-`1.0.0`
|
|
11
|
-
> development against a single pinned Effect v4
|
|
11
|
+
> development against a single pinned Effect v4 prerelease. Packages graduate to
|
|
12
12
|
> `1.0.0` once Effect `4.0.0` ships. To hold your own `effect` versions at
|
|
13
13
|
> exactly the ones the kit is built and tested against, install
|
|
14
14
|
> [`@effected/pnpm-plugin-effect`](https://www.npmjs.com/package/@effected/pnpm-plugin-effect).
|
|
@@ -193,12 +193,40 @@ const program = Effect.gen(function* () {
|
|
|
193
193
|
const doc = yield* CheckDocument;
|
|
194
194
|
yield* doc.report("build", CheckReport.make({ state: "running" }));
|
|
195
195
|
yield* doc.report("build", CheckReport.make({ state: "pass", outcome: "clean" }));
|
|
196
|
-
yield* doc.flush;
|
|
196
|
+
return yield* doc.flush;
|
|
197
197
|
});
|
|
198
|
-
// one write carrying the final state
|
|
198
|
+
// "written" — one write carrying the final state
|
|
199
|
+
// "unchanged" — the render was byte-identical to the last one, so nothing was sent
|
|
200
|
+
// "stale" — a newer run owns the document; this pass was dropped (see below)
|
|
199
201
|
```
|
|
200
202
|
|
|
201
|
-
The debounce is trailing with a max-wait, so a burst of reports coalesces into one write carrying the *final* state while a steady stream still surfaces progress.
|
|
203
|
+
The debounce is trailing with a max-wait, so a burst of reports coalesces into one write carrying the *final* state while a steady stream still surfaces progress.
|
|
204
|
+
|
|
205
|
+
A region can also carry metadata: `withRegions` takes `[key, content, meta]` triples, the pairs ride the region's own marker, and `entry(key)` reads them back verbatim. Metadata survives a writer that knows nothing about it — a region a call does not name keeps its content *and* its metadata — and never takes part in addressing a region, so changing it updates that region in place rather than growing a second one.
|
|
206
|
+
|
|
207
|
+
`CheckDocument` uses exactly that to survive two runs writing one document — a re-run overlapping the delayed run it replaced. Give the layer this run's `stamp` and a sink that can `read` as well as `write`, and each pass reconciles against the live document instead of a private shadow of what this process last wrote:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { CheckDocument } from "@effected/github-actions";
|
|
211
|
+
import type { Effect } from "effect";
|
|
212
|
+
|
|
213
|
+
declare const startedAt: string;
|
|
214
|
+
declare const runId: string;
|
|
215
|
+
declare const readComment: Effect.Effect<string | undefined>;
|
|
216
|
+
declare const writeComment: (text: string) => Effect.Effect<void>;
|
|
217
|
+
|
|
218
|
+
const layer = CheckDocument.layer({
|
|
219
|
+
namespace: "release-bot",
|
|
220
|
+
key: "checks",
|
|
221
|
+
render: (checks) => [...checks].map(([name, report]) => [name, report.state] as const),
|
|
222
|
+
sink: { write: writeComment, read: readComment },
|
|
223
|
+
stamp: { at: startedAt, runId }, // a per-run constant — mint it once at startup
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Every region the run writes then carries its `at` and `runId`, and a pass whose stamp is strictly older than the newest stamp already on the document is dropped rather than written — `flush` answers `"stale"` and the reconciler logs the drop once, at the transition. Equal stamps pass, so a run always may refine its own regions. Keeping the stamp constant is what preserves write suppression: the same run rendering the same state still produces byte-identical text.
|
|
228
|
+
|
|
229
|
+
`CheckState` is the vocabulary both of them speak, and it is deliberately wider than GitHub's conclusion set — `running` is a state rather than the absence of one, and `user_interaction_required` names a pipeline waiting on a human. `projectCheckState` maps each onto the wire form GitHub wants, as a status plus a conclusion exactly when the status is `completed`.
|
|
202
230
|
|
|
203
231
|
## Testing
|
|
204
232
|
|
|
@@ -239,9 +267,9 @@ const TestOutputs = ActionOutputs.layerTest({
|
|
|
239
267
|
- `OidcTokenIssuer` — the runner's OIDC token service, with unverified claim decoding for provenance predicates.
|
|
240
268
|
- `ActionsIdentityToken` / `ActionsProvenance` — the `@effected/sbom` seam: `layer` serves that package's `IdentityToken` contract from the runner's issuer, and `capture` builds a `SlsaProvenance` predicate out of the runner's OIDC claims.
|
|
241
269
|
- `GitHubMarkdown` — the escaping-safe writer for GitHub surfaces: `table`, `tableFor(schema)`, `heading`, `link`, `code`, `codeBlock`, `list`, `details` and `raw`, and the package's only importer of `@effected/markdown`.
|
|
242
|
-
- `ManagedDocument` — marker-delimited named regions in text a human also edits, for a sticky comment or a managed pull request description; content outside the regions survives byte-for-byte.
|
|
270
|
+
- `ManagedDocument` — marker-delimited named regions in text a human also edits, for a sticky comment or a managed pull request description; content outside the regions survives byte-for-byte, and each region can carry marker metadata that round-trips through `entry(key)` without ever addressing the region.
|
|
243
271
|
- `CheckState` / `projectCheckState` — the kit's check vocabulary, wider than GitHub's conclusions, and its projection onto the check-run wire form.
|
|
244
|
-
- `CheckDocument` — the debounced check-to-document reconciler: report states as they change, and one write lands carrying the final one.
|
|
272
|
+
- `CheckDocument` — the debounced check-to-document reconciler: report states as they change, and one write lands carrying the final one. `stamp` plus a readable sink lets two runs share one document, dropping a strictly older run's pass instead of clobbering the newer one; `flush` reports `written`, `unchanged` or `stale`.
|
|
245
273
|
- `ToolInstaller` — download, extract and cache a toolchain in the runner's tool cache, with no `@actions/tool-cache` dependency.
|
|
246
274
|
- `BlobStore` — durable `get`/`put`/`has` over bytes plus a metadata channel, backed by `layerS3` (SigV4-signed, no `@aws-sdk/*` dependency) or `GitHubCacheBlobStore.layer` (the runner's own cache); `layerMemory` runs the real envelope framing for tests.
|
|
247
275
|
|
package/index.d.ts
CHANGED
|
@@ -194,6 +194,26 @@ declare class ActionEnvironment extends ActionEnvironment_base {
|
|
|
194
194
|
* `undefined` means *not served*, so a suite cannot arrange `undefined` as
|
|
195
195
|
* the payload itself. A webhook payload is always a JSON object; the case
|
|
196
196
|
* does not arise, and reserving the sentinel keeps the argument optional.
|
|
197
|
+
*
|
|
198
|
+
* When a suite wants the **real read path** rather than a served value —
|
|
199
|
+
* exercising the actual `GITHUB_EVENT_PATH` read-and-parse — this member is
|
|
200
|
+
* the seam: it leaves `FileSystem` in `R` precisely so the suite chooses
|
|
201
|
+
* the filesystem. Compose it with `@effected/memfs`'s seeded in-memory
|
|
202
|
+
* volume rather than a hand-rolled stub: the volume honors the whole
|
|
203
|
+
* `FileSystem` contract, and an unseeded path fails typed `NotFound`
|
|
204
|
+
* instead of whatever a hand stub happens to answer.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* ```ts
|
|
208
|
+
* import { ActionEnvironment } from "@effected/github-actions";
|
|
209
|
+
* import { MemoryFileSystem } from "@effected/memfs";
|
|
210
|
+
* import { Layer } from "effect";
|
|
211
|
+
*
|
|
212
|
+
* const layer = Layer.effect(
|
|
213
|
+
* ActionEnvironment,
|
|
214
|
+
* ActionEnvironment.makeTest({ GITHUB_EVENT_PATH: "/event.json" }),
|
|
215
|
+
* ).pipe(Layer.provide(MemoryFileSystem.layerWith({ "/event.json": JSON.stringify({ action: "opened" }) })));
|
|
216
|
+
* ```
|
|
197
217
|
*/
|
|
198
218
|
static readonly makeTest: (overrides?: Readonly<Record<string, string>>, payload?: unknown) => Effect.Effect<ActionEnvironmentShape, never, FileSystem.FileSystem>;
|
|
199
219
|
/**
|
|
@@ -204,7 +224,10 @@ declare class ActionEnvironment extends ActionEnvironment_base {
|
|
|
204
224
|
* The stub is why the payload has to be served here rather than through the
|
|
205
225
|
* filesystem: `make` captures the filesystem at construction, so seeding
|
|
206
226
|
* `GITHUB_EVENT_PATH` through `overrides` would send the read to a noop
|
|
207
|
-
* filesystem and fail. Pass the payload as the second argument instead
|
|
227
|
+
* filesystem and fail. Pass the payload as the second argument instead —
|
|
228
|
+
* or, for a suite that wants the real read path over a full filesystem,
|
|
229
|
+
* compose {@link ActionEnvironment.makeTest} with `@effected/memfs`'s
|
|
230
|
+
* seeded volume; the recipe is on that member.
|
|
208
231
|
*
|
|
209
232
|
* @example
|
|
210
233
|
* ```ts
|
|
@@ -2389,41 +2412,108 @@ declare const CheckReport_base: Schema.Class<CheckReport, Schema.Struct<{
|
|
|
2389
2412
|
*/
|
|
2390
2413
|
declare class CheckReport extends CheckReport_base {}
|
|
2391
2414
|
declare const CheckDocumentError_base: Schema.Class<CheckDocumentError, Schema.TaggedStruct<"CheckDocumentError", {
|
|
2392
|
-
/** Which
|
|
2393
|
-
readonly kind: Schema.Literals<readonly ["render", "sink"]>;
|
|
2415
|
+
/** Which phase failed: regenerating the document, reading it back, or writing it. */
|
|
2416
|
+
readonly kind: Schema.Literals<readonly ["render", "read", "sink"]>;
|
|
2394
2417
|
/** The underlying failure. */
|
|
2395
2418
|
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
2396
2419
|
}>, import("effect/Cause").YieldableError>;
|
|
2397
2420
|
/**
|
|
2398
|
-
* Raised when the check document cannot be regenerated or written.
|
|
2421
|
+
* Raised when the check document cannot be regenerated, read back or written.
|
|
2399
2422
|
*
|
|
2400
2423
|
* @remarks
|
|
2401
2424
|
* `render` — projecting the registry onto the document failed, almost always
|
|
2402
2425
|
* because a reported `detail` contains a line the region scanner would read
|
|
2403
2426
|
* as a marker; the underlying `ManagedDocumentError` rides in `cause`.
|
|
2404
|
-
* `
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2427
|
+
* `read` — the sink's read-back of the live document failed; its error rides
|
|
2428
|
+
* in `cause`. `sink` — the consumer's write effect failed; its error rides in
|
|
2429
|
+
* `cause`. Callers of `flush` branch on `kind`; the background reconciler
|
|
2430
|
+
* never fails a fiber with this — it logs and retries on the next report.
|
|
2407
2431
|
*
|
|
2408
2432
|
* @public
|
|
2409
2433
|
*/
|
|
2410
2434
|
declare class CheckDocumentError extends CheckDocumentError_base {
|
|
2411
2435
|
get message(): string;
|
|
2412
2436
|
}
|
|
2437
|
+
declare const CheckDocumentStamp_base: Schema.Class<CheckDocumentStamp, Schema.Struct<{
|
|
2438
|
+
/** When the run started — ideally an ISO-8601 instant. */
|
|
2439
|
+
readonly at: Schema.String;
|
|
2440
|
+
/** The run's identifier — on GitHub, `GITHUB_RUN_ID`. */
|
|
2441
|
+
readonly runId: Schema.String;
|
|
2442
|
+
}>, {}>;
|
|
2443
|
+
/**
|
|
2444
|
+
* A run's identity for staleness ordering: when it started and which run it
|
|
2445
|
+
* is.
|
|
2446
|
+
*
|
|
2447
|
+
* @remarks
|
|
2448
|
+
* Two delayed workflows can race for one document — a re-run's stale pass
|
|
2449
|
+
* must not overwrite a newer run's regions. The ordering is **total**:
|
|
2450
|
+
* `at` compares first (as epoch milliseconds when both sides `Date.parse`
|
|
2451
|
+
* cleanly, lexically otherwise), `runId` breaks ties (numerically when both
|
|
2452
|
+
* parse to finite numbers, lexically otherwise), and two equal stamps compare
|
|
2453
|
+
* as at-least-as-recent — a run may refine its own regions.
|
|
2454
|
+
*
|
|
2455
|
+
* @public
|
|
2456
|
+
*/
|
|
2457
|
+
declare class CheckDocumentStamp extends CheckDocumentStamp_base {
|
|
2458
|
+
/**
|
|
2459
|
+
* Is `incoming` allowed to overwrite regions stamped `existing`?
|
|
2460
|
+
*
|
|
2461
|
+
* @remarks
|
|
2462
|
+
* Total and reflexive: equal stamps answer `true`, so a run always may
|
|
2463
|
+
* rewrite what it wrote itself. Only a strictly older `incoming` answers
|
|
2464
|
+
* `false`.
|
|
2465
|
+
*/
|
|
2466
|
+
static isAtLeastAsRecent(incoming: {
|
|
2467
|
+
readonly at: string;
|
|
2468
|
+
readonly runId: string;
|
|
2469
|
+
}, existing: {
|
|
2470
|
+
readonly at: string;
|
|
2471
|
+
readonly runId: string;
|
|
2472
|
+
}): boolean;
|
|
2473
|
+
}
|
|
2474
|
+
/**
|
|
2475
|
+
* What one reconcile pass did.
|
|
2476
|
+
*
|
|
2477
|
+
* @remarks
|
|
2478
|
+
* `written` — the render changed and the sink wrote it. `unchanged` — the
|
|
2479
|
+
* render was byte-identical, so no write was issued. `stale` — the document
|
|
2480
|
+
* already carries regions from a more recent run, so the pass was dropped
|
|
2481
|
+
* without writing (only possible when {@link CheckDocumentOptions.stamp} is
|
|
2482
|
+
* set). A plain literal union: callers that ignored `flush`'s old `void`
|
|
2483
|
+
* success stay compile-compatible.
|
|
2484
|
+
*
|
|
2485
|
+
* @public
|
|
2486
|
+
*/
|
|
2487
|
+
type CheckFlushOutcome = "written" | "unchanged" | "stale";
|
|
2413
2488
|
/**
|
|
2414
2489
|
* How a rendered document leaves the process: the narrow sink contract.
|
|
2415
2490
|
*
|
|
2416
2491
|
* @remarks
|
|
2417
|
-
*
|
|
2418
|
-
* what lets the sticky PR comment
|
|
2419
|
-
* description (`PATCH` on the pull
|
|
2420
|
-
*
|
|
2421
|
-
*
|
|
2422
|
-
*
|
|
2492
|
+
* The function form is one rendered document in, one write effect out —
|
|
2493
|
+
* nothing else. That is what lets the sticky PR comment
|
|
2494
|
+
* (`PullRequestComment.upsert`), the PR description (`PATCH` on the pull
|
|
2495
|
+
* request) and a test's recording `Ref` all fit the same reconciler. The
|
|
2496
|
+
* write's success value is ignored; a failure surfaces as
|
|
2497
|
+
* {@link CheckDocumentError} with `kind: "sink"`. Provide any services the
|
|
2498
|
+
* effects need when constructing the sink — the reconciler runs them as
|
|
2499
|
+
* given.
|
|
2500
|
+
*
|
|
2501
|
+
* The object form adds an optional `read`: the live document's current text,
|
|
2502
|
+
* fetched at the start of every pass and used as the base to reconcile
|
|
2503
|
+
* against instead of the last text this process wrote. `undefined` means
|
|
2504
|
+
* "nothing there yet" — a fresh document. That is what lets two runs share
|
|
2505
|
+
* one document: each pass sees what the *other* run wrote, not a private
|
|
2506
|
+
* shadow. A failed read surfaces as `kind: "read"`. A bare function is
|
|
2507
|
+
* exactly `{ write }`.
|
|
2423
2508
|
*
|
|
2424
2509
|
* @public
|
|
2425
2510
|
*/
|
|
2426
|
-
type CheckDocumentSink = (rendered: string) => Effect.Effect<unknown, unknown
|
|
2511
|
+
type CheckDocumentSink = ((rendered: string) => Effect.Effect<unknown, unknown>) | {
|
|
2512
|
+
/** Write one rendered document. */
|
|
2513
|
+
readonly write: (rendered: string) => Effect.Effect<unknown, unknown>;
|
|
2514
|
+
/** The live document's current text, or `undefined` for none yet. */
|
|
2515
|
+
readonly read?: Effect.Effect<string | undefined, unknown> | undefined;
|
|
2516
|
+
};
|
|
2427
2517
|
/**
|
|
2428
2518
|
* Options for {@link CheckDocument.layer}.
|
|
2429
2519
|
*
|
|
@@ -2454,6 +2544,27 @@ interface CheckDocumentOptions {
|
|
|
2454
2544
|
readonly render: (checks: ReadonlyMap<string, CheckReport>) => ReadonlyArray<readonly [key: string, content: string]>;
|
|
2455
2545
|
/** Where the rendered document goes. */
|
|
2456
2546
|
readonly sink: CheckDocumentSink;
|
|
2547
|
+
/**
|
|
2548
|
+
* This run's identity, for staleness ordering against other runs.
|
|
2549
|
+
*
|
|
2550
|
+
* @remarks
|
|
2551
|
+
* A **per-run constant** — mint it once at startup, never per pass. When
|
|
2552
|
+
* present, every region a pass writes carries `at`/`runId` metadata, and a
|
|
2553
|
+
* pass whose stamp is strictly older than the most recent stamp already on
|
|
2554
|
+
* the document is **dropped** rather than written, so a delayed re-run can
|
|
2555
|
+
* never clobber a newer run's regions. Equal stamps pass: a run may refine
|
|
2556
|
+
* its own regions.
|
|
2557
|
+
*
|
|
2558
|
+
* The constant is what preserves write suppression: the same run rendering
|
|
2559
|
+
* the same state produces byte-identical text (stamp included), so no
|
|
2560
|
+
* write is issued. The accepted corner is that a content-identical pass
|
|
2561
|
+
* therefore does not refresh the document's stamp to "now" — suppression
|
|
2562
|
+
* wins — which is sound because only strictly-older stamps drop.
|
|
2563
|
+
*/
|
|
2564
|
+
readonly stamp?: {
|
|
2565
|
+
readonly at: string;
|
|
2566
|
+
readonly runId: string;
|
|
2567
|
+
} | undefined;
|
|
2457
2568
|
/**
|
|
2458
2569
|
* Debounce parameters: `quiet` is the trailing settle window, `maxWait`
|
|
2459
2570
|
* the staleness bound under a steady stream of reports. Defaults: 500
|
|
@@ -2464,8 +2575,9 @@ interface CheckDocumentOptions {
|
|
|
2464
2575
|
readonly maxWait?: Duration.Input | undefined;
|
|
2465
2576
|
} | undefined;
|
|
2466
2577
|
/**
|
|
2467
|
-
* How long one sink write
|
|
2468
|
-
* `kind: "sink"
|
|
2578
|
+
* How long one sink write — or one read-back, when the sink carries a
|
|
2579
|
+
* `read` — may run before the pass fails with `kind: "sink"` (or
|
|
2580
|
+
* `kind: "read"`). Default: 30 seconds.
|
|
2469
2581
|
*
|
|
2470
2582
|
* @remarks
|
|
2471
2583
|
* The reconciler serializes passes behind one permit, and the finalizer's
|
|
@@ -2501,9 +2613,11 @@ interface CheckDocumentShape {
|
|
|
2501
2613
|
* @remarks
|
|
2502
2614
|
* Rendering unchanged state issues no write. Call it at the end of a run;
|
|
2503
2615
|
* the layer's finalizer also runs it, so the last burst of reports lands
|
|
2504
|
-
* even when the scope closes inside the quiet window.
|
|
2616
|
+
* even when the scope closes inside the quiet window. The outcome says
|
|
2617
|
+
* what the pass did — `written`, `unchanged` or `stale` — and callers
|
|
2618
|
+
* that ignore it stay exactly as they were.
|
|
2505
2619
|
*/
|
|
2506
|
-
readonly flush: Effect.Effect<
|
|
2620
|
+
readonly flush: Effect.Effect<CheckFlushOutcome, CheckDocumentError>;
|
|
2507
2621
|
}
|
|
2508
2622
|
declare const CheckDocument_base: Context.ServiceClass<CheckDocument, "@effected/github-actions/CheckDocument", CheckDocumentShape>;
|
|
2509
2623
|
/**
|
|
@@ -3587,11 +3701,13 @@ declare class GitHubToken {
|
|
|
3587
3701
|
//#region src/ManagedDocument.d.ts
|
|
3588
3702
|
declare const ManagedDocumentError_base: Schema.Class<ManagedDocumentError, Schema.TaggedStruct<"ManagedDocumentError", {
|
|
3589
3703
|
/** Which ambiguity or refusal was found. */
|
|
3590
|
-
readonly kind: Schema.Literals<readonly ["unterminatedRegion", "orphanedEnd", "overlappingRegions", "duplicateRegion", "markerInContent", "duplicateDeclaration"]>;
|
|
3704
|
+
readonly kind: Schema.Literals<readonly ["unterminatedRegion", "orphanedEnd", "overlappingRegions", "duplicateRegion", "markerInContent", "duplicateDeclaration", "invalidAttribute"]>;
|
|
3591
3705
|
/** 1-based line of the offending marker, for the structural kinds. */
|
|
3592
3706
|
readonly line: Schema.optionalKey<Schema.Number>;
|
|
3593
3707
|
/** The region key involved, when the failure names one. */
|
|
3594
3708
|
readonly key: Schema.optionalKey<Schema.String>;
|
|
3709
|
+
/** The offending metadata attribute's name, for `invalidAttribute`. */
|
|
3710
|
+
readonly attribute: Schema.optionalKey<Schema.String>;
|
|
3595
3711
|
}>, import("effect/Cause").YieldableError>;
|
|
3596
3712
|
/**
|
|
3597
3713
|
* Raised when a managed document cannot be read or regenerated without
|
|
@@ -3601,10 +3717,14 @@ declare const ManagedDocumentError_base: Schema.Class<ManagedDocumentError, Sche
|
|
|
3601
3717
|
* The four structural kinds (`unterminatedRegion`, `orphanedEnd`,
|
|
3602
3718
|
* `overlappingRegions`, `duplicateRegion`) surface at parse time and name an
|
|
3603
3719
|
* ambiguity whose silent resolution would corrupt content a human wrote by
|
|
3604
|
-
* hand. The
|
|
3720
|
+
* hand. The three declaration kinds surface when regions are applied:
|
|
3605
3721
|
* `markerInContent` refuses region content that itself contains a line the
|
|
3606
|
-
* scanner would read as a region marker,
|
|
3607
|
-
* one call that states two intentions for the same region
|
|
3722
|
+
* scanner would read as a region marker, `duplicateDeclaration` refuses
|
|
3723
|
+
* one call that states two intentions for the same region, and
|
|
3724
|
+
* `invalidAttribute` refuses region metadata whose name is outside the
|
|
3725
|
+
* `[A-Za-z][A-Za-z0-9_-]*` grammar or whose value contains `"` or a line
|
|
3726
|
+
* break — either would write a marker the region scanner could not read
|
|
3727
|
+
* back verbatim.
|
|
3608
3728
|
*
|
|
3609
3729
|
* @public
|
|
3610
3730
|
*/
|
|
@@ -3706,28 +3826,58 @@ declare class ManagedDocument extends ManagedDocument_base {
|
|
|
3706
3826
|
matches(text: string): boolean;
|
|
3707
3827
|
/** The content of one region, if the document carries it. */
|
|
3708
3828
|
region(key: string): Option.Option<string>;
|
|
3709
|
-
/**
|
|
3829
|
+
/**
|
|
3830
|
+
* One region's content and metadata together, if the document carries it.
|
|
3831
|
+
*
|
|
3832
|
+
* @remarks
|
|
3833
|
+
* `meta` is the region's `name="value"` marker attributes, empty when the
|
|
3834
|
+
* marker carries none. Metadata never participates in region
|
|
3835
|
+
* addressability — this looks up by key exactly as
|
|
3836
|
+
* {@link ManagedDocument.region} does.
|
|
3837
|
+
*/
|
|
3838
|
+
entry(key: string): Option.Option<{
|
|
3839
|
+
readonly content: string;
|
|
3840
|
+
readonly meta: Readonly<Record<string, string>>;
|
|
3841
|
+
}>;
|
|
3842
|
+
/**
|
|
3843
|
+
* Every region this document carries, in document order.
|
|
3844
|
+
*
|
|
3845
|
+
* @remarks
|
|
3846
|
+
* `meta` is always present: a region whose marker carries no attributes
|
|
3847
|
+
* reports an empty record, so callers never branch on absence.
|
|
3848
|
+
*/
|
|
3710
3849
|
get regions(): ReadonlyArray<{
|
|
3711
3850
|
readonly key: string;
|
|
3712
3851
|
readonly content: string;
|
|
3852
|
+
readonly meta: Readonly<Record<string, string>>;
|
|
3713
3853
|
}>;
|
|
3714
3854
|
/**
|
|
3715
3855
|
* The document with these regions replaced from current state.
|
|
3716
3856
|
*
|
|
3717
3857
|
* @remarks
|
|
3718
3858
|
* The synchronous primitive; {@link ManagedDocument.withRegions} derives
|
|
3719
|
-
* from it. Each entry pairs a region key with its full new content
|
|
3720
|
-
* region already present is **replaced in place**; a
|
|
3721
|
-
* yet is added; regions not named are left exactly as
|
|
3722
|
-
* byte outside a managed region survives untouched.
|
|
3723
|
-
* change nothing returns byte-identical text, which
|
|
3724
|
-
* compare-and-skip instead of issuing a write.
|
|
3859
|
+
* from it. Each entry pairs a region key with its full new content, plus
|
|
3860
|
+
* optional metadata. A region already present is **replaced in place**; a
|
|
3861
|
+
* region not present yet is added; regions not named are left exactly as
|
|
3862
|
+
* they are; and every byte outside a managed region survives untouched.
|
|
3863
|
+
* Applying entries that change nothing returns byte-identical text, which
|
|
3864
|
+
* is what lets a caller compare-and-skip instead of issuing a write.
|
|
3865
|
+
*
|
|
3866
|
+
* `meta` becomes `name="value"` attributes on the region's BEGIN marker.
|
|
3867
|
+
* It round-trips verbatim through {@link ManagedDocument.entry}, survives
|
|
3868
|
+
* writes by parties that do not know about it — a region not named in a
|
|
3869
|
+
* call keeps its content *and* its metadata untouched — and never
|
|
3870
|
+
* participates in region addressability: a region is found by key alone,
|
|
3871
|
+
* so changing metadata updates the region in place. A two-element entry
|
|
3872
|
+
* behaves exactly as it always has. Names must match
|
|
3873
|
+
* `[A-Za-z][A-Za-z0-9_-]*` and values may not contain `"` or a line
|
|
3874
|
+
* break; violations fail typed as `invalidAttribute`.
|
|
3725
3875
|
*
|
|
3726
3876
|
* Region keys share the name grammar `namespace` and `key` obey; a key
|
|
3727
3877
|
* outside it is a wiring error and throws at construction rather than
|
|
3728
3878
|
* surfacing in the typed channel.
|
|
3729
3879
|
*/
|
|
3730
|
-
withRegionsResult(entries: ReadonlyArray<readonly [key: string, content: string]>): Result.Result<ManagedDocument, ManagedDocumentError>;
|
|
3880
|
+
withRegionsResult(entries: ReadonlyArray<readonly [key: string, content: string, meta?: Readonly<Record<string, string>>]>): Result.Result<ManagedDocument, ManagedDocumentError>;
|
|
3731
3881
|
/**
|
|
3732
3882
|
* The document with these regions replaced, in `Effect`.
|
|
3733
3883
|
*
|
|
@@ -3735,7 +3885,7 @@ declare class ManagedDocument extends ManagedDocument_base {
|
|
|
3735
3885
|
* Defined in terms of {@link ManagedDocument.withRegionsResult} —
|
|
3736
3886
|
* synchronous callers can use that variant directly.
|
|
3737
3887
|
*/
|
|
3738
|
-
withRegions(entries: ReadonlyArray<readonly [key: string, content: string]>): Effect.Effect<ManagedDocument, ManagedDocumentError>;
|
|
3888
|
+
withRegions(entries: ReadonlyArray<readonly [key: string, content: string, meta?: Readonly<Record<string, string>>]>): Effect.Effect<ManagedDocument, ManagedDocumentError>;
|
|
3739
3889
|
/** The marker key a region is written under: `namespace.key.region`. */
|
|
3740
3890
|
private wireKey;
|
|
3741
3891
|
/** The region key back out of a wire key, for error reporting. */
|
|
@@ -4390,5 +4540,5 @@ declare class Secret {
|
|
|
4390
4540
|
static readonly adopt: (name: string) => Config.Config<Redacted.Redacted<string>>;
|
|
4391
4541
|
}
|
|
4392
4542
|
//#endregion
|
|
4393
|
-
export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, ChildEnv, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedProcessOps, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, type PathPrependEnv, type PathPrependOptions, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
|
|
4543
|
+
export { Action, ActionCache, ActionCacheError, type ActionCacheShape, ActionEnvironment, ActionEnvironmentError, type ActionEnvironmentShape, ActionInput, ActionLogger, type ActionLoggerShape, ActionOutputError, ActionOutputs, type ActionOutputsShape, type ActionRunOptions, ActionRuntime, type ActionServices, ActionState, ActionStateError, type ActionStateShape, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, type AnnotationProperties, Artifact, ArtifactError, type ArtifactItem, type ArtifactRef, type ArtifactShape, type Blob, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, type BlobStoreShape, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, type CheckDocumentOptions, type CheckDocumentShape, type CheckDocumentSink, CheckDocumentStamp, type CheckFlushOutcome, CheckReport, type CheckRunConclusion, type CheckRunProjection, CheckState, ChildEnv, type ClientLayerOptions, type DataBlobTransfer, DetachedProcess, DetachedProcessError, type DetachedProcessOps, type DetachedSpawnOptions, type DownloadOptions, type DownloadResult, DryRun, type DryRunShape, type ExtractOptions, type FileBlobTransfer, GitHubCacheBlobStore, GitHubContext, type GitHubHeadingDepth, type GitHubListOptions, GitHubMarkdown, type GitHubRowSchema, type GitHubSchemaTable, type GitHubSchemaTableColumn, type GitHubSchemaTableColumns, type GitHubSchemaTableFormatRequiredKeys, type GitHubSchemaTableFormattedColumn, type GitHubSchemaTableOptions, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, type ManagedDocumentSource, OidcClaims, OidcTokenError, OidcTokenIssuer, type OidcTokenIssuerShape, type PackageManagerInstallOptions, PackageManagerInstaller, PackageManagerInstallerError, type PackageManagerInstallerShape, type PathPrependEnv, type PathPrependOptions, ProcessId, type ProvisionFileOptions, type ProvisionOptions, type ProvisionedFile, type ReadOptions, type ReadinessOptions, RunnerContext, type S3Config, Secret, type ToolDownloadOptions, ToolInstaller, ToolInstallerError, type ToolInstallerShape, type UploadOptions, type UploadResult, type WithBufferOptions, type WithStepOptions, WorkflowCommand, describeCause, projectCheckState };
|
|
4394
4544
|
//# sourceMappingURL=index.d.ts.map
|
package/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { GitHubCacheBlobStore } from "./BlobStore.githubCache.js";
|
|
|
18
18
|
import { CacheKey, CacheKeyError } from "./CacheKey.js";
|
|
19
19
|
import { CheckState, projectCheckState } from "./CheckState.js";
|
|
20
20
|
import { ManagedDocument, ManagedDocumentError } from "./ManagedDocument.js";
|
|
21
|
-
import { CheckDocument, CheckDocumentError, CheckReport } from "./CheckDocument.js";
|
|
21
|
+
import { CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport } from "./CheckDocument.js";
|
|
22
22
|
import { ChildEnv } from "./ChildEnv.js";
|
|
23
23
|
import { DetachedProcess, DetachedProcessError, ProcessId } from "./DetachedProcess.js";
|
|
24
24
|
import { DryRun } from "./DryRun.js";
|
|
@@ -27,4 +27,4 @@ import { GitHubToken, GitHubTokenError } from "./GitHubToken.js";
|
|
|
27
27
|
import { ToolInstaller, ToolInstallerError } from "./ToolInstaller.js";
|
|
28
28
|
import { AmbientPackageManager, CachedPackageManager, InstalledPackageManager, PackageManagerInstaller, PackageManagerInstallerError } from "./PackageManagerInstaller.js";
|
|
29
29
|
|
|
30
|
-
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckReport, CheckState, ChildEnv, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
|
|
30
|
+
export { Action, ActionCache, ActionCacheError, ActionEnvironment, ActionEnvironmentError, ActionInput, ActionLogger, ActionOutputError, ActionOutputs, ActionRuntime, ActionState, ActionStateError, ActionsIdentityToken, ActionsProvenance, AmbientPackageManager, Artifact, ArtifactError, BlobEnvelope, BlobEnvelopeError, BlobStore, BlobStoreError, BlobTransferError, CacheKey, CacheKeyError, CachedPackageManager, CheckDocument, CheckDocumentError, CheckDocumentStamp, CheckReport, CheckState, ChildEnv, DetachedProcess, DetachedProcessError, DryRun, GitHubCacheBlobStore, GitHubContext, GitHubMarkdown, GitHubToken, GitHubTokenError, InstalledPackageManager, ManagedDocument, ManagedDocumentError, OidcClaims, OidcTokenError, OidcTokenIssuer, PackageManagerInstaller, PackageManagerInstallerError, ProcessId, RunnerContext, Secret, ToolInstaller, ToolInstallerError, WorkflowCommand, describeCause, projectCheckState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effected/github-actions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The GitHub Actions runtime: env, inputs, outputs, state, logging, cache, artifacts, blob storage, OIDC and the token bridge.",
|
|
6
6
|
"keywords": [
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@azure/storage-blob": "^12.33.0",
|
|
42
|
-
"@effected/github": "^0.
|
|
43
|
-
"@effected/glob": "^0.
|
|
44
|
-
"@effected/markdown": "^0.
|
|
45
|
-
"@effected/npm": "^0.
|
|
46
|
-
"@effected/sbom": "^0.
|
|
47
|
-
"@effected/templates": "^0.
|
|
42
|
+
"@effected/github": "^0.6.0",
|
|
43
|
+
"@effected/glob": "^0.4.0",
|
|
44
|
+
"@effected/markdown": "^0.6.0",
|
|
45
|
+
"@effected/npm": "^0.10.0",
|
|
46
|
+
"@effected/sbom": "^0.4.0",
|
|
47
|
+
"@effected/templates": "^0.4.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@effect/platform-node": "4.0.0-
|
|
51
|
-
"effect": "4.0.0-
|
|
50
|
+
"@effect/platform-node": "4.0.0-rc.109",
|
|
51
|
+
"effect": "4.0.0-rc.109"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=24.11.0"
|