@effected/github-actions 0.1.0 → 0.3.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/ActionCache.js +128 -17
- package/ActionInput.js +61 -8
- package/ActionLogger.js +7 -3
- package/ActionOutputs.js +71 -2
- package/ActionState.js +33 -6
- package/ActionsIdentityToken.js +61 -0
- package/ActionsProvenance.js +103 -0
- package/Artifact.js +2 -2
- package/BlobStore.githubCache.js +2 -1
- package/CacheKey.js +106 -8
- package/CheckDocument.js +193 -0
- package/CheckState.js +75 -0
- package/GitHubMarkdown.js +197 -0
- package/ManagedDocument.js +281 -0
- package/PackageManagerInstaller.js +494 -0
- package/README.md +241 -0
- package/Secret.js +34 -0
- package/ToolInstaller.js +98 -52
- package/index.d.ts +1612 -230
- package/index.js +9 -2
- package/package.json +7 -3
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { Effect, Option, Result, Schema } from "effect";
|
|
2
|
+
import { CommentStyle, SectionDialect, SectionDocument, SectionId } from "@effected/templates";
|
|
3
|
+
|
|
4
|
+
//#region src/ManagedDocument.ts
|
|
5
|
+
/**
|
|
6
|
+
* The grammar shared by a document's `namespace`, its `key` and every region
|
|
7
|
+
* key.
|
|
8
|
+
*
|
|
9
|
+
* @remarks
|
|
10
|
+
* Letters, digits, dashes and underscores, starting alphanumeric. Dots are
|
|
11
|
+
* excluded deliberately: the three parts are joined with `.` into the marker
|
|
12
|
+
* key written to the wire (`namespace.key.region`), and a dot inside a part
|
|
13
|
+
* would make two different documents spell the same marker.
|
|
14
|
+
*/
|
|
15
|
+
const NamePart = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9][A-Za-z0-9_-]*$/));
|
|
16
|
+
/**
|
|
17
|
+
* The marker vocabulary every managed document is written with: HTML comments
|
|
18
|
+
* — the one comment style GitHub renders invisibly — and a phrase distinct
|
|
19
|
+
* from `@effected/templates`' default, so a document that also carries
|
|
20
|
+
* ordinary `MANAGED SECTION` blocks never has them mistaken for regions.
|
|
21
|
+
*/
|
|
22
|
+
const REGION_DIALECT = SectionDialect.make({
|
|
23
|
+
phrase: "MANAGED REGION",
|
|
24
|
+
styles: [CommentStyle.html]
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* Raised when a managed document cannot be read or regenerated without
|
|
28
|
+
* guessing.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* The four structural kinds (`unterminatedRegion`, `orphanedEnd`,
|
|
32
|
+
* `overlappingRegions`, `duplicateRegion`) surface at parse time and name an
|
|
33
|
+
* ambiguity whose silent resolution would corrupt content a human wrote by
|
|
34
|
+
* hand. The two declaration kinds surface when regions are applied:
|
|
35
|
+
* `markerInContent` refuses region content that itself contains a line the
|
|
36
|
+
* scanner would read as a region marker, and `duplicateDeclaration` refuses
|
|
37
|
+
* one call that states two intentions for the same region.
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
var ManagedDocumentError = class extends Schema.TaggedErrorClass()("ManagedDocumentError", {
|
|
42
|
+
/** Which ambiguity or refusal was found. */
|
|
43
|
+
kind: Schema.Literals([
|
|
44
|
+
"unterminatedRegion",
|
|
45
|
+
"orphanedEnd",
|
|
46
|
+
"overlappingRegions",
|
|
47
|
+
"duplicateRegion",
|
|
48
|
+
"markerInContent",
|
|
49
|
+
"duplicateDeclaration"
|
|
50
|
+
]),
|
|
51
|
+
/** 1-based line of the offending marker, for the structural kinds. */
|
|
52
|
+
line: Schema.optionalKey(Schema.Number),
|
|
53
|
+
/** The region key involved, when the failure names one. */
|
|
54
|
+
key: Schema.optionalKey(Schema.String)
|
|
55
|
+
}) {
|
|
56
|
+
get message() {
|
|
57
|
+
const which = this.key === void 0 ? "" : ` for region "${this.key}"`;
|
|
58
|
+
const where = this.line === void 0 ? "" : ` at line ${this.line}`;
|
|
59
|
+
return `${KIND_PROSE[this.kind]}${which}${where}`;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const KIND_PROSE = {
|
|
63
|
+
unterminatedRegion: "Managed region is never closed",
|
|
64
|
+
orphanedEnd: "Region end marker closes no open region",
|
|
65
|
+
overlappingRegions: "Managed region opens inside another",
|
|
66
|
+
duplicateRegion: "Managed region appears twice",
|
|
67
|
+
markerInContent: "Region content contains a managed-region marker",
|
|
68
|
+
duplicateDeclaration: "Region was declared twice in one call"
|
|
69
|
+
};
|
|
70
|
+
/** Map a templates scan failure onto this surface's vocabulary. */
|
|
71
|
+
const STRUCTURAL_KIND = {
|
|
72
|
+
unterminatedSection: "unterminatedRegion",
|
|
73
|
+
orphanedEnd: "orphanedEnd",
|
|
74
|
+
overlappingSections: "overlappingRegions",
|
|
75
|
+
duplicateSection: "duplicateRegion"
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* A marker-delimited document: named regions a tool owns, inside text a human
|
|
79
|
+
* may also edit.
|
|
80
|
+
*
|
|
81
|
+
* @remarks
|
|
82
|
+
* The one primitive behind both the sticky PR comment and the managed PR
|
|
83
|
+
* description. A sentinel HTML comment (`<!-- namespace:key -->`) identifies
|
|
84
|
+
* the document; each region is delimited by HTML-comment markers keyed
|
|
85
|
+
* `namespace.key.region`; and everything outside the managed regions is
|
|
86
|
+
* preserved byte-for-byte on regeneration. Regions are **replaced from
|
|
87
|
+
* current state, never appended** — applying the same region twice yields one
|
|
88
|
+
* region carrying the latest content, which is what makes a check that
|
|
89
|
+
* resolves many times safe to re-render.
|
|
90
|
+
*
|
|
91
|
+
* The region engine is `@effected/templates`' pure `SectionDocument` —
|
|
92
|
+
* reused, not reinvented — fixed to the HTML comment style and a
|
|
93
|
+
* region-specific phrase. Everything here is string → string; the GitHub
|
|
94
|
+
* write path composes this with `PullRequestComment` (whose `CommentMarker`
|
|
95
|
+
* renders the *same* sentinel for the same namespace and key) or a PR-body
|
|
96
|
+
* update in `@effected/github`.
|
|
97
|
+
*
|
|
98
|
+
* Construct with {@link ManagedDocument.parseResult} — create-or-update in
|
|
99
|
+
* one call, so the consumer never branches on whether the document exists
|
|
100
|
+
* yet. `ManagedDocument.make` is the raw schema constructor and performs no
|
|
101
|
+
* structural validation.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { ManagedDocument } from "@effected/github-actions";
|
|
106
|
+
* import { Result } from "effect";
|
|
107
|
+
*
|
|
108
|
+
* const doc = ManagedDocument.parseResult({ namespace: "my-action", key: "release", text: fetched });
|
|
109
|
+
* if (Result.isSuccess(doc)) {
|
|
110
|
+
* const next = doc.success.withRegionsResult([
|
|
111
|
+
* ["header", "## Validating Release"],
|
|
112
|
+
* ["footer", "generated by my-action"],
|
|
113
|
+
* ]);
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @public
|
|
118
|
+
*/
|
|
119
|
+
var ManagedDocument = class ManagedDocument extends Schema.Class("ManagedDocument")({
|
|
120
|
+
/** Whose document this is. */
|
|
121
|
+
namespace: NamePart,
|
|
122
|
+
/** Which document, within that namespace. */
|
|
123
|
+
key: NamePart,
|
|
124
|
+
/** The document's full text, sentinel and regions included. */
|
|
125
|
+
text: Schema.String
|
|
126
|
+
}) {
|
|
127
|
+
/**
|
|
128
|
+
* Read a document out of existing text, or begin a fresh one.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* The synchronous primitive; {@link ManagedDocument.parse} derives from it.
|
|
132
|
+
* Absent text, empty text and text that carries no sentinel are all legal —
|
|
133
|
+
* the sentinel and regions are added by the first
|
|
134
|
+
* {@link ManagedDocument.withRegionsResult}. Only a *structurally corrupt*
|
|
135
|
+
* region layout fails, because every structural ambiguity is a case where
|
|
136
|
+
* a silent choice would destroy content a human wrote.
|
|
137
|
+
*/
|
|
138
|
+
static parseResult(source) {
|
|
139
|
+
const text = source.text ?? "";
|
|
140
|
+
const scanned = SectionDocument.parseResult(text, REGION_DIALECT);
|
|
141
|
+
if (Result.isFailure(scanned)) {
|
|
142
|
+
const failure = scanned.failure;
|
|
143
|
+
return Result.fail(new ManagedDocumentError({
|
|
144
|
+
kind: STRUCTURAL_KIND[failure.reason],
|
|
145
|
+
line: failure.line,
|
|
146
|
+
...failure.key === void 0 ? {} : { key: failure.key }
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
return Result.succeed(ManagedDocument.make({
|
|
150
|
+
namespace: source.namespace,
|
|
151
|
+
key: source.key,
|
|
152
|
+
text
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read a document out of existing text, in `Effect`.
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Defined in terms of {@link ManagedDocument.parseResult} — synchronous
|
|
160
|
+
* callers can use that variant directly.
|
|
161
|
+
*/
|
|
162
|
+
static parse = Effect.fn("ManagedDocument.parse")((source) => Effect.fromResult(ManagedDocument.parseResult(source)));
|
|
163
|
+
/** The HTML comment identifying this document, `<!-- namespace:key -->`. */
|
|
164
|
+
get sentinel() {
|
|
165
|
+
return `<!-- ${this.namespace}:${this.key} -->`;
|
|
166
|
+
}
|
|
167
|
+
/** Does this text carry the document's sentinel? */
|
|
168
|
+
matches(text) {
|
|
169
|
+
return text.includes(this.sentinel);
|
|
170
|
+
}
|
|
171
|
+
/** The content of one region, if the document carries it. */
|
|
172
|
+
region(key) {
|
|
173
|
+
const found = this.ownRegions().find((entry) => entry.key === key);
|
|
174
|
+
return found === void 0 ? Option.none() : Option.some(found.content);
|
|
175
|
+
}
|
|
176
|
+
/** Every region this document carries, in document order. */
|
|
177
|
+
get regions() {
|
|
178
|
+
return this.ownRegions();
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The document with these regions replaced from current state.
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* The synchronous primitive; {@link ManagedDocument.withRegions} derives
|
|
185
|
+
* from it. Each entry pairs a region key with its full new content. A
|
|
186
|
+
* region already present is **replaced in place**; a region not present
|
|
187
|
+
* yet is added; regions not named are left exactly as they are; and every
|
|
188
|
+
* byte outside a managed region survives untouched. Applying entries that
|
|
189
|
+
* change nothing returns byte-identical text, which is what lets a caller
|
|
190
|
+
* compare-and-skip instead of issuing a write.
|
|
191
|
+
*
|
|
192
|
+
* Region keys share the name grammar `namespace` and `key` obey; a key
|
|
193
|
+
* outside it is a wiring error and throws at construction rather than
|
|
194
|
+
* surfacing in the typed channel.
|
|
195
|
+
*/
|
|
196
|
+
withRegionsResult(entries) {
|
|
197
|
+
const parsed = SectionDocument.parseResult(this.text, REGION_DIALECT);
|
|
198
|
+
if (Result.isFailure(parsed)) {
|
|
199
|
+
const failure = parsed.failure;
|
|
200
|
+
return Result.fail(new ManagedDocumentError({
|
|
201
|
+
kind: STRUCTURAL_KIND[failure.reason],
|
|
202
|
+
line: failure.line,
|
|
203
|
+
...failure.key === void 0 ? {} : { key: failure.key }
|
|
204
|
+
}));
|
|
205
|
+
}
|
|
206
|
+
const declared = entries.map(([key, content]) => SectionId.make({
|
|
207
|
+
key: this.wireKey(key),
|
|
208
|
+
commentStyle: CommentStyle.html
|
|
209
|
+
}).section(content));
|
|
210
|
+
const reconciled = parsed.success.reconcile(declared);
|
|
211
|
+
if (Result.isFailure(reconciled)) {
|
|
212
|
+
const failure = reconciled.failure;
|
|
213
|
+
if (failure.reason === "unknownCommentStyle") throw new Error("ManagedDocument: the region dialect rejected its own comment style");
|
|
214
|
+
return Result.fail(new ManagedDocumentError({
|
|
215
|
+
kind: failure.reason,
|
|
216
|
+
key: this.regionKeyOf(failure.key)
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
const withSentinel = this.ensureSentinel(reconciled.success.text, parsed.success.eol);
|
|
220
|
+
return Result.succeed(ManagedDocument.make({
|
|
221
|
+
namespace: this.namespace,
|
|
222
|
+
key: this.key,
|
|
223
|
+
text: withSentinel
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The document with these regions replaced, in `Effect`.
|
|
228
|
+
*
|
|
229
|
+
* @remarks
|
|
230
|
+
* Defined in terms of {@link ManagedDocument.withRegionsResult} —
|
|
231
|
+
* synchronous callers can use that variant directly.
|
|
232
|
+
*/
|
|
233
|
+
withRegions(entries) {
|
|
234
|
+
return Effect.fromResult(this.withRegionsResult(entries));
|
|
235
|
+
}
|
|
236
|
+
/** The marker key a region is written under: `namespace.key.region`. */
|
|
237
|
+
wireKey(region) {
|
|
238
|
+
return `${this.namespace}.${this.key}.${region}`;
|
|
239
|
+
}
|
|
240
|
+
/** The region key back out of a wire key, for error reporting. */
|
|
241
|
+
regionKeyOf(wireKey) {
|
|
242
|
+
const prefix = `${this.namespace}.${this.key}.`;
|
|
243
|
+
return wireKey.startsWith(prefix) ? wireKey.slice(prefix.length) : wireKey;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* This document's own regions, in document order.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* A document a human already edited can be structurally corrupt, and a
|
|
250
|
+
* read has no error channel worth having — a corrupt document simply has
|
|
251
|
+
* no readable regions; {@link ManagedDocument.parseResult} is where
|
|
252
|
+
* corruption fails typed. Regions belonging to a *different* document in
|
|
253
|
+
* the same text (another namespace or key) are not this document's and are
|
|
254
|
+
* never reported.
|
|
255
|
+
*/
|
|
256
|
+
ownRegions() {
|
|
257
|
+
const parsed = SectionDocument.parseResult(this.text, REGION_DIALECT);
|
|
258
|
+
if (Result.isFailure(parsed)) return [];
|
|
259
|
+
const prefix = `${this.namespace}.${this.key}.`;
|
|
260
|
+
return parsed.success.sections.filter((placed) => placed.section.key.startsWith(prefix)).map((placed) => ({
|
|
261
|
+
key: placed.section.key.slice(prefix.length),
|
|
262
|
+
content: placed.section.content
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Append the sentinel when the text does not carry it yet.
|
|
267
|
+
*
|
|
268
|
+
* @remarks
|
|
269
|
+
* Appended at the end — the same position `PullRequestComment.upsert`
|
|
270
|
+
* writes its marker to — so a document rendered here and a comment written
|
|
271
|
+
* there agree about where the identity line lives.
|
|
272
|
+
*/
|
|
273
|
+
ensureSentinel(text, eol) {
|
|
274
|
+
if (text.includes(this.sentinel)) return text;
|
|
275
|
+
if (text.trim() === "") return `${this.sentinel}${eol}`;
|
|
276
|
+
return `${text.replace(/(?:\r?\n)+$/, "")}${eol}${eol}${this.sentinel}${eol}`;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
export { ManagedDocument, ManagedDocumentError };
|