@jr2/orchestrator 0.3.0 → 0.4.1
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/bin/server.ts +7 -4
- package/console/canvas.ts +27 -14
- package/console/components/app.ts +10 -1
- package/console/components/fleet.ts +39 -6
- package/console/components/machine-pane.ts +89 -2
- package/console/components/nav.ts +1 -1
- package/console/components/schema-form.ts +12 -4
- package/console/json-field.ts +23 -0
- package/console/main.ts +110 -39
- package/console/store.ts +221 -17
- package/console/style.css +90 -0
- package/package.json +4 -4
- package/src/actor.ts +502 -49
- package/src/ambient.ts +2 -0
- package/src/assembly-checks.ts +165 -0
- package/src/assembly-tar.ts +494 -0
- package/src/assembly.ts +721 -0
- package/src/chart.ts +2050 -0
- package/src/charts.ts +720 -0
- package/src/config.ts +152 -46
- package/src/content-hash.ts +105 -0
- package/src/custodian.lua +6 -5
- package/src/custodian.ts +71 -19
- package/src/customize.ts +216 -25
- package/src/effect-checks.ts +557 -0
- package/src/effect.ts +905 -0
- package/src/effector-wire.ts +525 -0
- package/src/fingerprint.ts +4 -3
- package/src/gate.ts +8 -0
- package/src/git-clone.ts +65 -0
- package/src/git-push.ts +336 -0
- package/src/harness-client.ts +245 -23
- package/src/held-secrets.ts +82 -26
- package/src/holds.ts +156 -0
- package/src/http-effect.ts +981 -0
- package/src/http.ts +496 -104
- package/src/images.ts +47 -21
- package/src/index.ts +43 -2
- package/src/instance.ts +444 -58
- package/src/kube-client.ts +333 -0
- package/src/machine-doc.ts +88 -6
- package/src/mount-digest.ts +53 -0
- package/src/names.ts +66 -8
- package/src/notices.ts +88 -0
- package/src/open-body.ts +1444 -0
- package/src/open-effect.ts +144 -0
- package/src/open.ts +4 -4
- package/src/parts.ts +530 -33
- package/src/party.ts +468 -0
- package/src/pick.ts +309 -0
- package/src/pool.ts +1 -1
- package/src/registration.ts +301 -17
- package/src/repo-fetch.ts +60 -85
- package/src/repos.ts +63 -74
- package/src/rest.ts +133 -0
- package/src/run-host.ts +1832 -98
- package/src/sandbox-effect.ts +891 -0
- package/src/sandbox-effector.ts +111 -0
- package/src/sandbox-kube.ts +1719 -0
- package/src/sandbox-watch.ts +260 -0
- package/src/secret-placement.ts +494 -0
- package/src/server.ts +281 -84
- package/src/setup.ts +589 -70
- package/src/size.ts +342 -0
- package/src/snapshot-store.ts +64 -13
- package/src/stub-harness.ts +5 -0
- package/src/sync-client.ts +54 -0
- package/src/timers.ts +178 -0
- package/src/tokens.ts +35 -4
- package/src/vocabulary.ts +66 -1
- package/src/wire.ts +156 -2
- package/src/workspace.ts +529 -194
- package/src/sandbox-kubectl.ts +0 -1108
package/src/git-push.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// The push credentials (ADR-0051, ADR-0067): a `push()` spends the `git.credentials` entry its
|
|
2
|
+
// Repo Slot's url matches — longest prefix, the attach match — and that entry's push credential
|
|
3
|
+
// reaches the pod by reference, placed by the pod's maker when the url is known.
|
|
4
|
+
//
|
|
5
|
+
// - `jr2 up` writes each entry's push and sign credentials into that entry's own Secret,
|
|
6
|
+
// `jr2-git-push-<hash of match>`, never per Workspace: Flux's key names where Flux has them
|
|
7
|
+
// (`username`/`password` for a token, `identity`/`identity.pub`/`known_hosts` for an ssh key),
|
|
8
|
+
// `sign.identity` or `sign.private.asc` for the signing key, and `entry.json` — what the entry
|
|
9
|
+
// is, names only. A token is the `.env` value; a key is copied as data from the Secret the entry
|
|
10
|
+
// names, never decoded. And it writes the manifest `git-push.json` in `jr2-held`: which entries
|
|
11
|
+
// it wrote and what each can spend, names only.
|
|
12
|
+
// - At provision the Orchestrator resolves each pushed slot's url against the fence and names the
|
|
13
|
+
// matched entry's Secret on the Sandbox CR (`gitCredentials`, the Repo CR's `secretRef` shape);
|
|
14
|
+
// the operator projects it into the Sandbox Effector at `@git/<its name>/`. Static, per-run and
|
|
15
|
+
// `customize()`-bound urls take this one path, and the Orchestrator reads no value.
|
|
16
|
+
//
|
|
17
|
+
// The refusal is one function for both sides: `jr2 up` for a static Binding, from the entry as
|
|
18
|
+
// written; the provision for any url, from what `jr2 up` wrote.
|
|
19
|
+
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { readFile } from "node:fs/promises";
|
|
22
|
+
import { matchCredential, type GitCredential } from "./config.ts";
|
|
23
|
+
import { repoIdentity } from "./repo-identity.ts";
|
|
24
|
+
|
|
25
|
+
/** The `jr2-held` key of the push manifest. */
|
|
26
|
+
export const GIT_PUSH_MANIFEST_KEY = "git-push.json";
|
|
27
|
+
/** The label every entry's push Secret carries, so a stale one is found. */
|
|
28
|
+
export const LABEL_GIT_PUSH = "jr2.dev/git-push";
|
|
29
|
+
|
|
30
|
+
/** One entry's push Secret: deterministic in `match`, so a redeploy finds its own and two entries
|
|
31
|
+
* never share one. Apart from the clone token's Secret (`jr2-git-<hash>`), which the Orchestrator
|
|
32
|
+
* writes for the cache agent. */
|
|
33
|
+
export function gitPushSecretName(match: string): string {
|
|
34
|
+
return `jr2-git-push-${createHash("sha256").update(match).digest("hex").slice(0, 8)}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The keys of an entry's push Secret — the operator projects exactly these, each when present. */
|
|
38
|
+
export const GIT_PUSH_KEYS = {
|
|
39
|
+
entry: "entry.json",
|
|
40
|
+
username: "username",
|
|
41
|
+
password: "password",
|
|
42
|
+
identity: "identity",
|
|
43
|
+
identityPub: "identity.pub",
|
|
44
|
+
knownHosts: "known_hosts",
|
|
45
|
+
signSsh: "sign.identity",
|
|
46
|
+
signGpg: "sign.private.asc",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export type PushKind = "ssh" | "token";
|
|
50
|
+
|
|
51
|
+
/** One entry's push Secret as `jr2 up` wrote it: names only. */
|
|
52
|
+
export type GitPushEntry = {
|
|
53
|
+
match: string;
|
|
54
|
+
/** Its place in `git.credentials`: a tie on the match's length goes to the first. */
|
|
55
|
+
order: number;
|
|
56
|
+
secret: string;
|
|
57
|
+
/** What it can push with. */
|
|
58
|
+
push: PushKind[];
|
|
59
|
+
sign?: "ssh" | "gpg";
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** `git-push.json`: every entry `jr2 up` wrote a push Secret for. */
|
|
63
|
+
export type GitPushManifest = { version: 1; entries: GitPushEntry[] };
|
|
64
|
+
|
|
65
|
+
/** The push credentials an entry declares: its `push` field alone when it has one; otherwise the
|
|
66
|
+
* clone credentials it holds, each spent over its own scheme. None: it admits a clone, never a push. */
|
|
67
|
+
export function declaredPushKinds(entry: GitCredential): PushKind[] {
|
|
68
|
+
if (entry.push) return ["sshKey" in entry.push ? "ssh" : "token"];
|
|
69
|
+
return [...(entry.sshKey ? (["ssh"] as const) : []), ...(entry.token ? (["token"] as const) : [])];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** What a push of one url needs from the entry it matches. */
|
|
73
|
+
export type PushNeed = {
|
|
74
|
+
/** For the message: `workflow "w": push "p" pushes Repo Slot "app" (url)`. */
|
|
75
|
+
at: string;
|
|
76
|
+
url: string;
|
|
77
|
+
sign: boolean;
|
|
78
|
+
/** It squashes: unsigned, the entry's `committer` makes the commit (ADR-0067). */
|
|
79
|
+
squash?: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Why a push of `need.url` cannot be spent, with the fix that works, or undefined. `kinds` says
|
|
84
|
+
* what an entry can push with — as written (`jr2 up`, `declaredPushKinds`) or as placed (the
|
|
85
|
+
* provision, the manifest; undefined when `jr2 up` placed nothing for it); `signs` whether it holds
|
|
86
|
+
* a signing key. Refused: a local path and `git://` (jr2 never pushes there), `http://` (a
|
|
87
|
+
* credential over cleartext http is the credential in the clear: a token goes over https only and
|
|
88
|
+
* an ssh key over ssh only), a url no entry matches, an entry whose credentials the url's scheme
|
|
89
|
+
* cannot spend, `sign: true` on an entry with no `sign` field, and an unsigned squash on an entry
|
|
90
|
+
* with neither `sign` nor `committer` (ADR-0067: the squashed commit is new, and needs a committer).
|
|
91
|
+
*
|
|
92
|
+
* Each fix is one the next check accepts: the match it names is a prefix of the url's identity;
|
|
93
|
+
* an `http://` url is told the url its entry CAN push to, or the https url and the token to add;
|
|
94
|
+
* a field it names to add never takes away a credential the entry already pushes with.
|
|
95
|
+
*/
|
|
96
|
+
export function pushRefusal(
|
|
97
|
+
need: PushNeed,
|
|
98
|
+
list: readonly GitCredential[],
|
|
99
|
+
kinds: (entry: GitCredential) => readonly PushKind[] | undefined,
|
|
100
|
+
signs: (entry: GitCredential) => boolean,
|
|
101
|
+
): { refusal: string } | { entry: GitCredential } {
|
|
102
|
+
const { at, url } = need;
|
|
103
|
+
const { scheme, identity } = repoIdentity(url);
|
|
104
|
+
if (scheme === "local") {
|
|
105
|
+
return {
|
|
106
|
+
refusal: `${at}, a local path — jr2 never pushes to one: git's local transport runs that repository's config and hooks (ADR-0067)`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (scheme === "git") {
|
|
110
|
+
return { refusal: `${at} over git://, which carries no credential — bind the slot over https or ssh (ADR-0051)` };
|
|
111
|
+
}
|
|
112
|
+
const entry = matchCredential(identity, list);
|
|
113
|
+
const held = entry === undefined ? [] : (kinds(entry) ?? []);
|
|
114
|
+
if (scheme === "http") {
|
|
115
|
+
const why =
|
|
116
|
+
`${at} over http://, and jr2 sends a push credential over https or ssh only — a token over http is the ` +
|
|
117
|
+
"token in the clear, and no ssh key is spent over http";
|
|
118
|
+
if (entry === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
refusal:
|
|
121
|
+
`${why}; bind the slot to the https url, and add the entry it then spends: ` +
|
|
122
|
+
`git: { credentials: [{ match: "${matchFor(identity)}", token: "${TOKEN_ENV}" }] } (ADR-0051)`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (held.includes("token")) {
|
|
126
|
+
return {
|
|
127
|
+
refusal: `${why}; bind the slot to the https url (git.credentials entry "${entry.match}" then pushes with its token) (ADR-0051)`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (held.includes("ssh")) {
|
|
131
|
+
return {
|
|
132
|
+
refusal: `${why}; bind the slot to the ssh url (git.credentials entry "${entry.match}" then pushes with its ssh key) (ADR-0051)`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
refusal:
|
|
137
|
+
`${why}; bind the slot to the https url, and add ${addLine(entry, "token")} to git.credentials entry ` +
|
|
138
|
+
`"${entry.match}" — it holds no push credential (ADR-0051)`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const want: PushKind = scheme === "ssh" ? "ssh" : "token";
|
|
142
|
+
if (entry === undefined) {
|
|
143
|
+
const field = want === "ssh" ? `sshKey: "${SSH_SECRET}"` : `token: "${TOKEN_ENV}"`;
|
|
144
|
+
return {
|
|
145
|
+
refusal:
|
|
146
|
+
`${at}, and no git.credentials entry matches "${identity}" — a push spends the entry its url ` +
|
|
147
|
+
`matches; add one: git: { credentials: [{ match: "${matchFor(identity)}", ${field} }] } (ADR-0051)`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (!held.includes(want)) {
|
|
151
|
+
const other: PushKind = want === "ssh" ? "token" : "ssh";
|
|
152
|
+
const why = `${at}, and git.credentials entry "${entry.match}" holds no credential a push over ${scheme} can spend`;
|
|
153
|
+
// Its `push` field is the one credential it pushes with, and the field names one: the url that
|
|
154
|
+
// spends it is the fix, as a second credential there would take the first away.
|
|
155
|
+
if (entry.push !== undefined && held.includes(other)) {
|
|
156
|
+
return {
|
|
157
|
+
refusal:
|
|
158
|
+
`${why} — its push field names ${other === "ssh" ? "an ssh key" : "a token"}, and a push field names ` +
|
|
159
|
+
`one credential; bind the slot to ${other === "ssh" ? "the ssh url" : "the https url"} (ADR-0051)`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
refusal:
|
|
164
|
+
held.length === 0
|
|
165
|
+
? `${why} — an entry with none admits a clone, never a push; add ${addLine(entry, want)} to it (ADR-0051)`
|
|
166
|
+
: `${why}; add ${addLine(entry, want)} to it (ADR-0051)`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (need.sign && !signs(entry)) {
|
|
170
|
+
return {
|
|
171
|
+
refusal:
|
|
172
|
+
`${at} signed, and git.credentials entry "${entry.match}" holds no sign field — add ` +
|
|
173
|
+
`sign: { sshKey: "<a Secret with identity>", name: "<committer name>", email: "<committer email>" } ` +
|
|
174
|
+
"to it; the email must belong to the forge account that holds the key (ADR-0051, ADR-0067)",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (need.squash && !need.sign && entry.sign === undefined && entry.committer === undefined) {
|
|
178
|
+
return {
|
|
179
|
+
refusal:
|
|
180
|
+
`${at} squashed without sign, and git.credentials entry "${entry.match}" holds neither sign nor committer ` +
|
|
181
|
+
"— the squashed commit is new and needs a committer; add " +
|
|
182
|
+
`committer: { name: "<committer name>", email: "<committer email>" } to it, or sign the push — ` +
|
|
183
|
+
`push({ squash: true, sign: true }) — and add sign: { sshKey: "<a Secret with identity>", ` +
|
|
184
|
+
`name: "<committer name>", email: "<committer email>" } to it (ADR-0051, ADR-0067)`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { entry };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const TOKEN_ENV = "<the env var that holds the token>";
|
|
191
|
+
const SSH_SECRET = "<a Secret with identity and known_hosts>";
|
|
192
|
+
|
|
193
|
+
/** The `match` a new entry needs to catch `identity`: its host and every directory above the
|
|
194
|
+
* repository — always a prefix of the identity (`gitea.local/app` → `gitea.local/`). */
|
|
195
|
+
function matchFor(identity: string): string {
|
|
196
|
+
return identity.slice(0, identity.lastIndexOf("/") + 1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The field that gives `entry` a push credential of `kind` and keeps each one it pushes with: a
|
|
200
|
+
* clone credential (`token`, `sshKey`) when it holds one and no `push` field — a clone credential
|
|
201
|
+
* pushes too, and a `push` field would replace it — else a `push` field. */
|
|
202
|
+
function addLine(entry: GitCredential, kind: PushKind): string {
|
|
203
|
+
const value = kind === "ssh" ? `sshKey: "${SSH_SECRET}"` : `token: "${TOKEN_ENV}"`;
|
|
204
|
+
if (entry.push === undefined && (entry.token !== undefined || entry.sshKey !== undefined)) return value;
|
|
205
|
+
return `push: { ${value} }`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The refusal against the manifest `jr2 up` wrote: what the provision checks for any url. An entry
|
|
209
|
+
* whose config holds what the push needs, and whose Secret `jr2 up` wrote without it — a token
|
|
210
|
+
* unset, a key's Secret absent at the last `jr2 up` — is refused as that, never as a field to add. */
|
|
211
|
+
export function placedPushRefusal(
|
|
212
|
+
need: PushNeed,
|
|
213
|
+
list: readonly GitCredential[],
|
|
214
|
+
manifest: GitPushManifest,
|
|
215
|
+
): { refusal: string } | { entry: GitPushEntry } {
|
|
216
|
+
const placed = (entry: GitCredential) => manifest.entries.find((e) => e.match === entry.match);
|
|
217
|
+
const out = pushRefusal(
|
|
218
|
+
need,
|
|
219
|
+
list,
|
|
220
|
+
(entry) => placed(entry)?.push,
|
|
221
|
+
(entry) => placed(entry)?.sign !== undefined,
|
|
222
|
+
);
|
|
223
|
+
if (!("refusal" in out)) return { entry: placed(out.entry)! };
|
|
224
|
+
const written = pushRefusal(need, list, declaredPushKinds, (entry) => entry.sign !== undefined);
|
|
225
|
+
// An http:// url is never pushed: the url to bind instead is read from the entry as written, so
|
|
226
|
+
// a token whose variable was unset is named as the entry's, and the https url then says so.
|
|
227
|
+
if (repoIdentity(need.url).scheme === "http" && "refusal" in written) return written;
|
|
228
|
+
// As written, would the entry push? Then what is missing is what `jr2 up` could not place.
|
|
229
|
+
if ("refusal" in written) return out;
|
|
230
|
+
const entry = written.entry;
|
|
231
|
+
const what =
|
|
232
|
+
placed(entry) === undefined
|
|
233
|
+
? "no push credential"
|
|
234
|
+
: need.sign && placed(entry)!.sign === undefined
|
|
235
|
+
? "no signing key"
|
|
236
|
+
: "no credential for this url's scheme";
|
|
237
|
+
return {
|
|
238
|
+
refusal:
|
|
239
|
+
`${need.at}, and jr2 up placed ${what} for git.credentials entry "${entry.match}" — its token's variable was ` +
|
|
240
|
+
"not set, or a Secret it names did not exist, when jr2 up last ran; set it or create it, and run jr2 up again " +
|
|
241
|
+
"(ADR-0051)",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** One key of an entry's push Secret, and where its value comes from: a token's value, or a key of
|
|
246
|
+
* the Secret the entry names, copied as data. */
|
|
247
|
+
export type GitPushSource =
|
|
248
|
+
| { key: string; literal: string }
|
|
249
|
+
| { key: string; from: { secret: string; key: string }; optional?: boolean };
|
|
250
|
+
|
|
251
|
+
/** One entry's push Secret, as `jr2 up` plans it before it reads the cluster. */
|
|
252
|
+
export type GitPushPlan = {
|
|
253
|
+
entry: GitPushEntry;
|
|
254
|
+
sources: GitPushSource[];
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Each `git.credentials` entry's push Secret (ADR-0051): the entries whose declared push credentials
|
|
259
|
+
* `jr2 up` can place. A token whose variable is unset is left out of its entry, as a kind it
|
|
260
|
+
* cannot push with; an entry with nothing left is not written. `skipped` says why, by match.
|
|
261
|
+
*/
|
|
262
|
+
export function gitPushPlansOf(
|
|
263
|
+
list: readonly GitCredential[],
|
|
264
|
+
env: (name: string) => string | undefined,
|
|
265
|
+
): { plans: GitPushPlan[]; skipped: Map<string, string> } {
|
|
266
|
+
const plans: GitPushPlan[] = [];
|
|
267
|
+
const skipped = new Map<string, string>();
|
|
268
|
+
list.forEach((entry, order) => {
|
|
269
|
+
const sources: GitPushSource[] = [];
|
|
270
|
+
const push: PushKind[] = [];
|
|
271
|
+
const token = entry.push ? ("token" in entry.push ? entry.push.token : undefined) : entry.token;
|
|
272
|
+
const sshKey = entry.push ? ("sshKey" in entry.push ? entry.push.sshKey : undefined) : entry.sshKey;
|
|
273
|
+
if (sshKey !== undefined) {
|
|
274
|
+
push.push("ssh");
|
|
275
|
+
sources.push(
|
|
276
|
+
{ key: GIT_PUSH_KEYS.identity, from: { secret: sshKey, key: "identity" } },
|
|
277
|
+
{ key: GIT_PUSH_KEYS.identityPub, from: { secret: sshKey, key: "identity.pub" }, optional: true },
|
|
278
|
+
{ key: GIT_PUSH_KEYS.knownHosts, from: { secret: sshKey, key: "known_hosts" } },
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (token !== undefined) {
|
|
282
|
+
const value = env(token);
|
|
283
|
+
if (value) {
|
|
284
|
+
push.push("token");
|
|
285
|
+
sources.push(
|
|
286
|
+
{ key: GIT_PUSH_KEYS.username, literal: "x-access-token" },
|
|
287
|
+
{ key: GIT_PUSH_KEYS.password, literal: value },
|
|
288
|
+
);
|
|
289
|
+
} else {
|
|
290
|
+
skipped.set(entry.match, `its token's variable ${token} is not set — set it in .env`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (push.length === 0) return;
|
|
294
|
+
const sign = entry.sign;
|
|
295
|
+
if (sign) {
|
|
296
|
+
sources.push(
|
|
297
|
+
"gpgKey" in sign
|
|
298
|
+
? { key: GIT_PUSH_KEYS.signGpg, from: { secret: sign.gpgKey, key: "private.asc" } }
|
|
299
|
+
: { key: GIT_PUSH_KEYS.signSsh, from: { secret: sign.sshKey, key: "identity" } },
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const placed: GitPushEntry = {
|
|
303
|
+
match: entry.match,
|
|
304
|
+
order,
|
|
305
|
+
secret: gitPushSecretName(entry.match),
|
|
306
|
+
push: push.sort(),
|
|
307
|
+
...(sign ? { sign: "gpgKey" in sign ? ("gpg" as const) : ("ssh" as const) } : {}),
|
|
308
|
+
};
|
|
309
|
+
const entryJson = {
|
|
310
|
+
match: entry.match,
|
|
311
|
+
order,
|
|
312
|
+
push: placed.push,
|
|
313
|
+
...(sign ? { sign: { format: placed.sign, name: sign.name, email: sign.email } } : {}),
|
|
314
|
+
...(entry.committer ? { committer: { name: entry.committer.name, email: entry.committer.email } } : {}),
|
|
315
|
+
};
|
|
316
|
+
sources.unshift({ key: GIT_PUSH_KEYS.entry, literal: JSON.stringify(entryJson) });
|
|
317
|
+
plans.push({ entry: placed, sources });
|
|
318
|
+
});
|
|
319
|
+
return { plans, skipped };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The manifest the provision reads, or none when `jr2 up` wrote no push Secret. */
|
|
323
|
+
export async function readGitPushManifest(path: string): Promise<GitPushManifest> {
|
|
324
|
+
let raw: string;
|
|
325
|
+
try {
|
|
326
|
+
raw = await readFile(path, "utf8");
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return { version: 1, entries: [] };
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
const parsed = JSON.parse(raw) as GitPushManifest;
|
|
332
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
|
|
333
|
+
throw new Error(`${path} is not a push manifest jr2 reads (version 1)`);
|
|
334
|
+
}
|
|
335
|
+
return parsed;
|
|
336
|
+
}
|