@minnovate/shadow-publisher 0.6.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-publisher",
|
|
3
3
|
"displayName": "Git Publisher",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.8.0",
|
|
5
5
|
"description": "Publish an app folder's Git history to the serverless Git publishing backend and keep it updated (\"publish my app\", \"update my app\", \"fetch the engineered version\").",
|
|
6
6
|
"author": { "name": "git-publisher project" },
|
|
7
7
|
"keywords": ["git", "publish", "deploy", "serverless"]
|
|
@@ -56,24 +56,16 @@ changing their default.
|
|
|
56
56
|
|
|
57
57
|
## 2. First publication of a folder
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
There is no separate first step: run step 3 in the app folder, adding `--name "<app name>"` the
|
|
60
|
+
first time. A folder that is not published yet gets its application and repository from that same
|
|
61
|
+
`sync` (the portal makes the application first, in one request), and is then published.
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
If not associated:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
publisher init . --name "<app name>" --json
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`init` prepares the folder with an explicit file selection: it excludes `.git`, `node_modules`,
|
|
72
|
-
build caches, `.env*`, keys/certificates and files larger than 50 MB, and it respects
|
|
73
|
-
`.gitignore`. Show the user the `onboarding.excluded` list so they know what was left out (the
|
|
74
|
-
exclusion rules are a safety net, not a secret scanner). Then publish it with step 3.
|
|
63
|
+
The first `sync` prepares the folder with an explicit file selection: it excludes `.git`,
|
|
64
|
+
`node_modules`, build caches, `.env*`, keys/certificates and files larger than 50 MB, and it
|
|
65
|
+
respects `.gitignore`. Show the user the `linked` result and the excluded files it reports so they
|
|
66
|
+
know what was left out (the exclusion rules are a safety net, not a secret scanner).
|
|
75
67
|
|
|
76
|
-
## 3. Every
|
|
68
|
+
## 3. Every publish, the first one included
|
|
77
69
|
|
|
78
70
|
```bash
|
|
79
71
|
publisher sync . --message "<a few words on what changed>" --timeout 90 --json
|
|
@@ -20,6 +20,28 @@ publish it follows the app through received → checked → going live → live
|
|
|
20
20
|
result. `merged` says whether anything
|
|
21
21
|
published elsewhere was brought in (`"fast-forward"` or `"merge"`); mention it when it is not null.
|
|
22
22
|
|
|
23
|
+
## When it waits for an answer
|
|
24
|
+
|
|
25
|
+
Sometimes the app's preparation asks one business question before it can go on (for example
|
|
26
|
+
whether to use the organisation's data). `follow` then stops with `kind: "waiting"` and
|
|
27
|
+
`next.command`. Read the question:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
publisher question . --json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Tell the person `questions[].text` and the `label` of each option, and ask which they want. Answer
|
|
34
|
+
with the option's `value`, never its label, only once they have chosen — or directly when they
|
|
35
|
+
told you what to answer:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
publisher answer <question id>=<value> --json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A value that is not an option is refused before anything is sent; `error.details.questions` lists
|
|
42
|
+
the choices. Exit 4 means this sign-in may not answer (an owner or admin can); exit 3 means ask
|
|
43
|
+
again. Then `publisher follow . --publication <id> --json` to see it go live.
|
|
44
|
+
|
|
23
45
|
## Just looking
|
|
24
46
|
|
|
25
47
|
To say where the folder stands without changing anything:
|
package/dist/publisher.cjs
CHANGED
|
@@ -97,6 +97,11 @@ var ERROR_CODES = [
|
|
|
97
97
|
// code rather than FORBIDDEN: a caller can act on it (sign in again, choosing an organisation),
|
|
98
98
|
// and a test that could not tell it from "you may not" would prove nothing about the check.
|
|
99
99
|
"ORGANISATION_REQUIRED",
|
|
100
|
+
// A repository is created only by the platform's application maker (the portal), for a person,
|
|
101
|
+
// after it has made the application the repository will belong to. Its own code: a terminal that
|
|
102
|
+
// meets it is an older client calling this route directly, and can say "upgrade" rather than
|
|
103
|
+
// "you may not".
|
|
104
|
+
"APPLICATION_REQUIRED",
|
|
100
105
|
// repositories
|
|
101
106
|
"REPOSITORY_NOT_FOUND",
|
|
102
107
|
"REPOSITORY_CONFLICT",
|
|
@@ -139,6 +144,7 @@ var HTTP_STATUS_FOR_CODE = {
|
|
|
139
144
|
FORBIDDEN: 403,
|
|
140
145
|
REF_FORBIDDEN: 403,
|
|
141
146
|
ORGANISATION_REQUIRED: 403,
|
|
147
|
+
APPLICATION_REQUIRED: 403,
|
|
142
148
|
REPOSITORY_NOT_FOUND: 404,
|
|
143
149
|
REPOSITORY_CONFLICT: 409,
|
|
144
150
|
REPOSITORY_ARCHIVED: 409,
|
|
@@ -2809,6 +2815,12 @@ var PrincipalRecord = object({
|
|
|
2809
2815
|
principal: PrincipalId,
|
|
2810
2816
|
disabled: boolean2(),
|
|
2811
2817
|
disabledAt: _default(nullable(IsoDate), null),
|
|
2818
|
+
/**
|
|
2819
|
+
* When the credentials tenant made the change this record reflects (ADR-0017), and what a later
|
|
2820
|
+
* event is ordered against: EventBridge does not order deliveries, and a late enable must not undo
|
|
2821
|
+
* a newer disable. Null on a record written by hand, which any event supersedes.
|
|
2822
|
+
*/
|
|
2823
|
+
changedAt: _default(nullable(IsoDate), null),
|
|
2812
2824
|
note: _default(nullable(string2().check(_maxLength(256))), null)
|
|
2813
2825
|
});
|
|
2814
2826
|
|
|
@@ -2848,6 +2860,8 @@ var CreateRepositoryRequest = object({
|
|
|
2848
2860
|
objectFormat: _default(ObjectFormat, "sha1"),
|
|
2849
2861
|
grants: _default(array(Grant).check(_maxLength(200)), [])
|
|
2850
2862
|
});
|
|
2863
|
+
var RepositoryIdRequest = object({ creationId: CreationId });
|
|
2864
|
+
var RepositoryIdResponse = object({ repoId: RepoId });
|
|
2851
2865
|
var RepositoryInfo = object({
|
|
2852
2866
|
repoId: RepoId,
|
|
2853
2867
|
name: string2(),
|
|
@@ -3585,12 +3599,12 @@ async function resolveRevs(dir, revs, opts = {}) {
|
|
|
3585
3599
|
const out = revs.map(
|
|
3586
3600
|
(r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
|
|
3587
3601
|
);
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3590
|
-
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input:
|
|
3602
|
+
const ask4 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
|
|
3603
|
+
if (ask4.length === 0) return out;
|
|
3604
|
+
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask4.map((i) => revs[i]).join("\0") + "\0", ...opts });
|
|
3591
3605
|
const records = stdout.toString("utf8").split("\0");
|
|
3592
3606
|
const misses = [];
|
|
3593
|
-
for (const [j, i] of
|
|
3607
|
+
for (const [j, i] of ask4.entries()) {
|
|
3594
3608
|
const record2 = records[j] ?? "";
|
|
3595
3609
|
if (record2 && !/ (missing|ambiguous)$/.test(record2)) out[i] = record2;
|
|
3596
3610
|
else if (!revs[i].includes("^{")) misses.push(i);
|
|
@@ -3990,7 +4004,7 @@ async function noteAcceptedPublication(directory, outcome, now = /* @__PURE__ */
|
|
|
3990
4004
|
if (!oid) return;
|
|
3991
4005
|
await advanceWorkspaceBase(directory, { oid, generation: outcome.resultGeneration }, now);
|
|
3992
4006
|
}
|
|
3993
|
-
async function adoptWorkspace(client, directory,
|
|
4007
|
+
async function adoptWorkspace(client, directory, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
3994
4008
|
const dir = (0, import_node_path3.resolve)(directory);
|
|
3995
4009
|
const existing = await getWorkspaceMapping(dir);
|
|
3996
4010
|
if (existing) return existing;
|
|
@@ -3999,13 +4013,13 @@ async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ *
|
|
|
3999
4013
|
if (!a) throw new PublisherError("LOCAL_STATE", "this folder is not associated with a published repository; run init first");
|
|
4000
4014
|
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
4001
4015
|
if (!branch) throw new PublisherError("LOCAL_STATE", "HEAD is detached; check out the branch this folder publishes, then run sync again");
|
|
4002
|
-
const repo = await client.getRepository(a.repoId,
|
|
4016
|
+
const repo = await client.getRepository(a.repoId, call2);
|
|
4003
4017
|
const remoteOid = repo.refs[`refs/heads/${branch}`] ?? null;
|
|
4004
4018
|
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
4005
4019
|
let baseOid = null;
|
|
4006
4020
|
let baseGeneration = 0;
|
|
4007
4021
|
if (remoteOid) {
|
|
4008
|
-
if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote },
|
|
4022
|
+
if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call2);
|
|
4009
4023
|
const haveRemote = await objectExists(dir, remoteOid);
|
|
4010
4024
|
if (!tip) {
|
|
4011
4025
|
baseOid = haveRemote ? remoteOid : null;
|
|
@@ -4064,15 +4078,15 @@ async function removeStalePartials(target, repoId) {
|
|
|
4064
4078
|
}
|
|
4065
4079
|
return removed;
|
|
4066
4080
|
}
|
|
4067
|
-
async function checkoutWorkspace(client, opts,
|
|
4081
|
+
async function checkoutWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
4068
4082
|
const target = (0, import_node_path3.resolve)(opts.directory);
|
|
4069
4083
|
const remote = opts.remote ?? "publisher";
|
|
4070
4084
|
const empty2 = await isEmptyDir(target);
|
|
4071
4085
|
if (empty2 === false) throw new PublisherError("LOCAL_STATE", `${target} exists and is not empty; checkout never overlays an existing folder`);
|
|
4072
4086
|
await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
|
|
4073
4087
|
const removed = await removeStalePartials(target, opts.repoId);
|
|
4074
|
-
if (removed.length)
|
|
4075
|
-
const repo = await client.getRepository(opts.repoId,
|
|
4088
|
+
if (removed.length) call2.onProgress?.({ phase: "checkout", message: `removed ${removed.length} interrupted checkout(s)` });
|
|
4089
|
+
const repo = await client.getRepository(opts.repoId, call2);
|
|
4076
4090
|
const branch = opts.branch ?? repo.head.symbolic.replace(/^refs\/heads\//, "");
|
|
4077
4091
|
const tip = repo.refs[`refs/heads/${branch}`] ?? null;
|
|
4078
4092
|
if (!tip && Object.keys(repo.refs).length > 0) throw new PublisherError("LOCAL_STATE", `branch ${branch} does not exist on the server`);
|
|
@@ -4083,7 +4097,7 @@ async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */
|
|
|
4083
4097
|
await runGit(["init", "-q", `--initial-branch=${branch}`, partial2]);
|
|
4084
4098
|
await writeAssociation(partial2, { endpoint: client.endpoint, repoId: opts.repoId, remote });
|
|
4085
4099
|
if (tip) {
|
|
4086
|
-
await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote },
|
|
4100
|
+
await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call2);
|
|
4087
4101
|
const tracking = `refs/remotes/${remote}/${branch}`;
|
|
4088
4102
|
const have = await revParse(partial2, tracking);
|
|
4089
4103
|
if (have !== tip) throw new PublisherError("LOCAL_STATE", `fetched ${tracking} is ${have ?? "missing"}, expected ${tip}; retry the checkout`);
|
|
@@ -4121,7 +4135,7 @@ async function countCommits(dir, range) {
|
|
|
4121
4135
|
const r = await runGit(["rev-list", "--count", range], { cwd: dir, allowFailure: true });
|
|
4122
4136
|
return r.code === 0 ? Number(r.stdout.toString("utf8").trim()) : 0;
|
|
4123
4137
|
}
|
|
4124
|
-
async function inspectWorkspace(client, directory,
|
|
4138
|
+
async function inspectWorkspace(client, directory, call2 = {}) {
|
|
4125
4139
|
const dir = (0, import_node_path3.resolve)(directory);
|
|
4126
4140
|
const mapping = await getWorkspaceMapping(dir);
|
|
4127
4141
|
const pending = await listPending(dir).catch(() => []);
|
|
@@ -4144,7 +4158,7 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4144
4158
|
const hasLocalWork = dirty || untrackedPaths.length > 0;
|
|
4145
4159
|
const checkedOut = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
4146
4160
|
const local = { branch: checkedOut, tip, head, dirty, untracked: untrackedPaths.length, untrackedPaths: untrackedPaths.slice(0, 10), commitsAheadOfBase: 0 };
|
|
4147
|
-
const repo = await client.getRepository(mapping.repoId,
|
|
4161
|
+
const repo = await client.getRepository(mapping.repoId, call2);
|
|
4148
4162
|
const remoteOid = repo.refs[branchRef] ?? null;
|
|
4149
4163
|
const remote = { oid: remoteOid, generation: repo.generation };
|
|
4150
4164
|
if (mapping.baseOid && tip) local.commitsAheadOfBase = await countCommits(dir, `${mapping.baseOid}..${tip}`);
|
|
@@ -4162,7 +4176,7 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4162
4176
|
}
|
|
4163
4177
|
if (!tip) return done("remote_ahead", hasLocalWork ? `local branch is unborn and there are ${work}; move them aside, then sync` : "local branch is unborn; sync to fast-forward");
|
|
4164
4178
|
if (!await objectExists(dir, remoteOid)) {
|
|
4165
|
-
await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote },
|
|
4179
|
+
await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call2);
|
|
4166
4180
|
if (!await objectExists(dir, remoteOid)) return done("remote_ahead", "server tip could not be fetched; retry");
|
|
4167
4181
|
}
|
|
4168
4182
|
if (remoteOid === tip)
|
|
@@ -4189,10 +4203,10 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4189
4203
|
merge2.conflicts.map((c) => c.path)
|
|
4190
4204
|
);
|
|
4191
4205
|
}
|
|
4192
|
-
async function syncWorkspace(client, opts,
|
|
4206
|
+
async function syncWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
4193
4207
|
const dir = (0, import_node_path3.resolve)(opts.directory);
|
|
4194
|
-
await adoptWorkspace(client, dir,
|
|
4195
|
-
const before = await inspectWorkspace(client, dir,
|
|
4208
|
+
await adoptWorkspace(client, dir, call2, now);
|
|
4209
|
+
const before = await inspectWorkspace(client, dir, call2);
|
|
4196
4210
|
if (!before.mapping) throw new PublisherError("LOCAL_STATE", "not a workspace; run checkout");
|
|
4197
4211
|
const mapping = before.mapping;
|
|
4198
4212
|
const branchRef = `refs/heads/${mapping.branch}`;
|
|
@@ -4216,7 +4230,7 @@ async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new
|
|
|
4216
4230
|
updated = await advanceWorkspaceBase(dir, { oid: before.remote.oid, generation: before.remote.generation }, now);
|
|
4217
4231
|
}
|
|
4218
4232
|
}
|
|
4219
|
-
const after = applied ? (await inspectWorkspace(client, dir,
|
|
4233
|
+
const after = applied ? (await inspectWorkspace(client, dir, call2)).state : before.state;
|
|
4220
4234
|
return { before, applied, mapping: updated, after };
|
|
4221
4235
|
}
|
|
4222
4236
|
|
|
@@ -4493,6 +4507,69 @@ async function updateEntry(url2, change) {
|
|
|
4493
4507
|
await writeCredentials(file);
|
|
4494
4508
|
}
|
|
4495
4509
|
|
|
4510
|
+
// ../client/src/application.ts
|
|
4511
|
+
var APPLICATION_CREATION = "application-creation";
|
|
4512
|
+
var ATTEMPTS = 3;
|
|
4513
|
+
var REFUSALS = {
|
|
4514
|
+
"repository-limit": "LIMIT_EXCEEDED",
|
|
4515
|
+
"repository-conflict": "REPOSITORY_CONFLICT"
|
|
4516
|
+
};
|
|
4517
|
+
async function createApplication(input, opts) {
|
|
4518
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4519
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/applications`;
|
|
4520
|
+
const body = { creationId: input.creationId ?? newCreationId(), name: input.name, defaultBranch: input.defaultBranch ?? "main" };
|
|
4521
|
+
let refreshed = false;
|
|
4522
|
+
let lastOutage = "";
|
|
4523
|
+
for (let attempt = 0; attempt < ATTEMPTS; ) {
|
|
4524
|
+
const answer = await post(fetchImpl, url2, await opts.token(), body);
|
|
4525
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4526
|
+
refreshed = true;
|
|
4527
|
+
if (await opts.refresh()) continue;
|
|
4528
|
+
}
|
|
4529
|
+
if (answer.status === 200 || answer.status === 201) return read(answer.body, answer.status === 201);
|
|
4530
|
+
const code = isRecord(answer.body) && typeof answer.body["error"] === "string" ? answer.body["error"] : "";
|
|
4531
|
+
if (answer.status === 401) throw new PublisherError("UNAUTHORIZED", 'the portal did not accept this credential; run "publisher login" again');
|
|
4532
|
+
if (answer.status === 403) throw new PublisherError("FORBIDDEN", `the portal refused to make an application here (${code || "forbidden"})`);
|
|
4533
|
+
if (answer.status === 409 && REFUSALS[code] !== void 0)
|
|
4534
|
+
throw new PublisherError(REFUSALS[code], `the portal refused the repository (${code})`, { details: { portal: code } });
|
|
4535
|
+
if (answer.status === 400 || answer.status === 409)
|
|
4536
|
+
throw new PublisherError("INVALID_REQUEST", `the portal refused the request (${code || answer.status})`, { details: { portal: code } });
|
|
4537
|
+
lastOutage = answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}`;
|
|
4538
|
+
attempt += 1;
|
|
4539
|
+
if (attempt < ATTEMPTS) await new Promise((resolve3) => setTimeout(resolve3, (opts.retryDelayMs ?? 1e3) * attempt));
|
|
4540
|
+
}
|
|
4541
|
+
throw new PublisherError("UNAVAILABLE", `could not make the application: ${lastOutage}; run init again`, { retryable: true });
|
|
4542
|
+
}
|
|
4543
|
+
function read(body, created) {
|
|
4544
|
+
const application = isRecord(body) ? body["application"] : void 0;
|
|
4545
|
+
const repository = RepositoryInfo.safeParse(isRecord(body) ? body["repository"] : void 0);
|
|
4546
|
+
if (!isRecord(application) || typeof application["appId"] !== "string" || !repository.success) {
|
|
4547
|
+
throw new PublisherError("UNAVAILABLE", "the portal answered with an application this version cannot read");
|
|
4548
|
+
}
|
|
4549
|
+
return { appId: application["appId"], created, repository: repository.data };
|
|
4550
|
+
}
|
|
4551
|
+
async function post(fetchImpl, url2, token, body) {
|
|
4552
|
+
try {
|
|
4553
|
+
const res = await fetchImpl(url2, {
|
|
4554
|
+
method: "POST",
|
|
4555
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", "content-type": "application/json" },
|
|
4556
|
+
body: JSON.stringify(body),
|
|
4557
|
+
signal: AbortSignal.timeout(3e4)
|
|
4558
|
+
});
|
|
4559
|
+
const text = await res.text();
|
|
4560
|
+
try {
|
|
4561
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
4562
|
+
} catch {
|
|
4563
|
+
return { status: res.status, body: void 0 };
|
|
4564
|
+
}
|
|
4565
|
+
} catch {
|
|
4566
|
+
return { status: 0, body: null };
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
function isRecord(value) {
|
|
4570
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4496
4573
|
// ../client/src/transport.ts
|
|
4497
4574
|
function assertCredentialEndpoint(endpoint) {
|
|
4498
4575
|
let url2;
|
|
@@ -4609,14 +4686,14 @@ var RUN_STAGES = ["received", "running", "deploying", "live"];
|
|
|
4609
4686
|
var STAGE_WORDS = { received: "received", running: "checked", deploying: "going live", live: "live" };
|
|
4610
4687
|
var STATES = ["pending", "in-progress", "done", "failed", "skipped"];
|
|
4611
4688
|
function readStages(run) {
|
|
4612
|
-
if (!
|
|
4689
|
+
if (!isRecord2(run)) return { malformed: "a run that is not an object" };
|
|
4613
4690
|
if (!Object.hasOwn(run, "stages")) return null;
|
|
4614
4691
|
const stages = run["stages"];
|
|
4615
4692
|
if (!Array.isArray(stages) || stages.length !== RUN_STAGES.length) return { malformed: "stages that are not exactly four" };
|
|
4616
4693
|
const out = [];
|
|
4617
4694
|
for (const [index, entry] of stages.entries()) {
|
|
4618
4695
|
const stage = RUN_STAGES[index];
|
|
4619
|
-
if (!
|
|
4696
|
+
if (!isRecord2(entry) || entry["stage"] !== stage) return { malformed: `stage ${index + 1} is not ${stage}` };
|
|
4620
4697
|
const state = entry["state"];
|
|
4621
4698
|
if (!STATES.includes(state)) return { malformed: `stage ${stage} has an unknown state` };
|
|
4622
4699
|
const at = entry["at"];
|
|
@@ -4659,17 +4736,18 @@ async function followRun(input, opts) {
|
|
|
4659
4736
|
const run = soleRun(answer.body);
|
|
4660
4737
|
if ("detail" in run) return { kind: "malformed", detail: run.detail };
|
|
4661
4738
|
if (run.run !== null) {
|
|
4662
|
-
const
|
|
4663
|
-
if (
|
|
4664
|
-
if ("malformed" in
|
|
4665
|
-
last =
|
|
4666
|
-
for (const entry of
|
|
4739
|
+
const read2 = readStages(run.run);
|
|
4740
|
+
if (read2 === null) return { kind: "unavailable", reason: "no-stages" };
|
|
4741
|
+
if ("malformed" in read2) return { kind: "malformed", detail: read2.malformed };
|
|
4742
|
+
last = read2.stages;
|
|
4743
|
+
for (const entry of read2.stages) {
|
|
4667
4744
|
if (entry.state === "pending" || seen.get(entry.stage) === entry.state) continue;
|
|
4668
4745
|
seen.set(entry.stage, entry.state);
|
|
4669
4746
|
opts.onStage?.(entry);
|
|
4670
4747
|
}
|
|
4671
|
-
|
|
4672
|
-
|
|
4748
|
+
if (isRecord2(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read2.stages };
|
|
4749
|
+
const end = ending(read2.stages);
|
|
4750
|
+
if (end !== null) return { kind: "settled", ending: end, failedAt: read2.stages.find((s) => s.state === "failed")?.word ?? null, stages: read2.stages };
|
|
4673
4751
|
}
|
|
4674
4752
|
lastError = null;
|
|
4675
4753
|
} else {
|
|
@@ -4680,7 +4758,7 @@ async function followRun(input, opts) {
|
|
|
4680
4758
|
}
|
|
4681
4759
|
}
|
|
4682
4760
|
function soleRun(body) {
|
|
4683
|
-
if (!
|
|
4761
|
+
if (!isRecord2(body) || !Array.isArray(body["runs"])) return { detail: "an answer with no list of runs" };
|
|
4684
4762
|
const runs = body["runs"];
|
|
4685
4763
|
if (runs.length > 1) return { detail: "more than the one run this publication started" };
|
|
4686
4764
|
return { run: runs[0] ?? null };
|
|
@@ -4725,7 +4803,88 @@ async function confirmPortal(portal, endpoint, fetchImpl = fetch) {
|
|
|
4725
4803
|
return null;
|
|
4726
4804
|
}
|
|
4727
4805
|
}
|
|
4728
|
-
function
|
|
4806
|
+
function isRecord2(value) {
|
|
4807
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
// ../client/src/preparation.ts
|
|
4811
|
+
var PREPARATION = "preparation";
|
|
4812
|
+
var STATUSES = ["queued", "running", "needs-choice", "ready", "blocked", "cancelled"];
|
|
4813
|
+
function readPreparation(input, opts) {
|
|
4814
|
+
return call(input, opts, void 0);
|
|
4815
|
+
}
|
|
4816
|
+
function answerPreparation(input, opts) {
|
|
4817
|
+
return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
|
|
4818
|
+
}
|
|
4819
|
+
async function call(input, opts, body) {
|
|
4820
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4821
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
|
|
4822
|
+
let refreshed = false;
|
|
4823
|
+
for (; ; ) {
|
|
4824
|
+
const answer = await ask2(fetchImpl, url2, await opts.token(), body);
|
|
4825
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4826
|
+
refreshed = true;
|
|
4827
|
+
if (await opts.refresh()) continue;
|
|
4828
|
+
}
|
|
4829
|
+
if (answer.status === 401) return { kind: "unauthorized" };
|
|
4830
|
+
if (answer.status === 403) return { kind: "forbidden" };
|
|
4831
|
+
if (answer.status === 409) return { kind: "moved-on" };
|
|
4832
|
+
if (answer.status === 404) {
|
|
4833
|
+
const code = isRecord3(answer.body) ? answer.body["error"] : void 0;
|
|
4834
|
+
return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
|
|
4835
|
+
}
|
|
4836
|
+
if (answer.status === 200) {
|
|
4837
|
+
const view = readView(answer.body);
|
|
4838
|
+
return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
|
|
4839
|
+
}
|
|
4840
|
+
return { kind: "unavailable", detail: answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}` };
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4843
|
+
function readView(body) {
|
|
4844
|
+
if (!isRecord3(body)) return { detail: "an answer that is not a preparation" };
|
|
4845
|
+
const { id, revision, status, message, questions, publication } = body;
|
|
4846
|
+
if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
|
|
4847
|
+
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
|
|
4848
|
+
if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
|
|
4849
|
+
if (typeof message !== "string") return { detail: "a preparation with no message" };
|
|
4850
|
+
if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
|
|
4851
|
+
const read2 = [];
|
|
4852
|
+
for (const q of questions) {
|
|
4853
|
+
if (!isRecord3(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
|
|
4854
|
+
return { detail: "a question that cannot be answered" };
|
|
4855
|
+
}
|
|
4856
|
+
const options = [];
|
|
4857
|
+
for (const o of q["options"]) {
|
|
4858
|
+
if (!isRecord3(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
|
|
4859
|
+
options.push({ value: o["value"], label: o["label"] });
|
|
4860
|
+
}
|
|
4861
|
+
read2.push({ id: q["id"], text: q["text"], options });
|
|
4862
|
+
}
|
|
4863
|
+
let pub = null;
|
|
4864
|
+
if (isRecord3(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
|
|
4865
|
+
pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
|
|
4866
|
+
}
|
|
4867
|
+
return { id, revision, status, message, questions: read2, publication: pub };
|
|
4868
|
+
}
|
|
4869
|
+
async function ask2(fetchImpl, url2, token, body) {
|
|
4870
|
+
try {
|
|
4871
|
+
const res = await fetchImpl(url2, {
|
|
4872
|
+
method: body === void 0 ? "GET" : "POST",
|
|
4873
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
4874
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
4875
|
+
signal: AbortSignal.timeout(3e4)
|
|
4876
|
+
});
|
|
4877
|
+
const text = await res.text();
|
|
4878
|
+
try {
|
|
4879
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
4880
|
+
} catch {
|
|
4881
|
+
return { status: res.status, body: void 0 };
|
|
4882
|
+
}
|
|
4883
|
+
} catch {
|
|
4884
|
+
return { status: 0, body: null };
|
|
4885
|
+
}
|
|
4886
|
+
}
|
|
4887
|
+
function isRecord3(value) {
|
|
4729
4888
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4730
4889
|
}
|
|
4731
4890
|
|
|
@@ -4968,13 +5127,13 @@ async function uploadPack(fetchImpl, upload, packPath, size, opts) {
|
|
|
4968
5127
|
}
|
|
4969
5128
|
|
|
4970
5129
|
// ../client/src/publish.ts
|
|
4971
|
-
async function publish(ctx, repoId, remote, opts,
|
|
4972
|
-
const progress =
|
|
5130
|
+
async function publish(ctx, repoId, remote, opts, call2) {
|
|
5131
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
4973
5132
|
const { repoDir } = opts;
|
|
4974
5133
|
const store2 = await openPending(repoDir);
|
|
4975
5134
|
const pendings = await store2.list();
|
|
4976
5135
|
progress({ phase: "resolve", message: "reading repository state" });
|
|
4977
|
-
const repo = await ctx.getRepository(repoId,
|
|
5136
|
+
const repo = await ctx.getRepository(repoId, call2);
|
|
4978
5137
|
const specs = opts.updates ?? [await defaultUpdate(repoDir)];
|
|
4979
5138
|
let refUpdates = [];
|
|
4980
5139
|
let nothingNew = null;
|
|
@@ -4990,9 +5149,9 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
4990
5149
|
const sameIntent = !nothingNew && pending2.baseGeneration === repo.generation && canonicalJson(refUpdates) === canonicalJson(pending2.refUpdates);
|
|
4991
5150
|
if (sameIntent && (pending2.pack === null || await packFileUsable(pending2))) {
|
|
4992
5151
|
progress({ phase: "resolve", message: `resuming publication ${pending2.publicationId}` });
|
|
4993
|
-
return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts,
|
|
5152
|
+
return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call2, true);
|
|
4994
5153
|
}
|
|
4995
|
-
const outcome = await resolvePending(ctx, repoDir, remote, pending2,
|
|
5154
|
+
const outcome = await resolvePending(ctx, repoDir, remote, pending2, call2, store2);
|
|
4996
5155
|
if (outcome.state === "finalizing" || outcome.state === "unknown") {
|
|
4997
5156
|
throw new PublisherError("OUTCOME_UNKNOWN", `publication ${pending2.publicationId} is still being processed; run status before publishing again`, {
|
|
4998
5157
|
retryable: true,
|
|
@@ -5000,7 +5159,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5000
5159
|
});
|
|
5001
5160
|
}
|
|
5002
5161
|
if (outcome.state === "accepted") {
|
|
5003
|
-
const fresh = await ctx.getRepository(repoId,
|
|
5162
|
+
const fresh = await ctx.getRepository(repoId, call2);
|
|
5004
5163
|
let remaining = [];
|
|
5005
5164
|
try {
|
|
5006
5165
|
remaining = await resolveUpdates(repoDir, fresh, specs);
|
|
@@ -5008,7 +5167,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5008
5167
|
if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
|
|
5009
5168
|
}
|
|
5010
5169
|
if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
|
|
5011
|
-
return publish(ctx, repoId, remote, opts,
|
|
5170
|
+
return publish(ctx, repoId, remote, opts, call2);
|
|
5012
5171
|
}
|
|
5013
5172
|
}
|
|
5014
5173
|
if (nothingNew) throw nothingNew;
|
|
@@ -5022,7 +5181,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5022
5181
|
const exclude = [];
|
|
5023
5182
|
for (const info of await batchCheck(repoDir, remoteOids)) if (info.type !== "missing") exclude.push(info.oid);
|
|
5024
5183
|
const outPath = store2.packPath(publicationId);
|
|
5025
|
-
const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...
|
|
5184
|
+
const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call2.signal ? { signal: call2.signal } : {} });
|
|
5026
5185
|
pack = { sha256: built.sha256, size: built.size, path: outPath };
|
|
5027
5186
|
progress({ phase: "pack", message: `packed ${built.objectCount} objects (${built.size} bytes)`, bytes: built.size, total: built.size });
|
|
5028
5187
|
}
|
|
@@ -5038,7 +5197,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5038
5197
|
finalized: false
|
|
5039
5198
|
};
|
|
5040
5199
|
await store2.write(pending);
|
|
5041
|
-
return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5200
|
+
return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, false);
|
|
5042
5201
|
}
|
|
5043
5202
|
async function packFileUsable(p) {
|
|
5044
5203
|
if (!p.pack) return true;
|
|
@@ -5049,9 +5208,9 @@ async function packFileUsable(p) {
|
|
|
5049
5208
|
return false;
|
|
5050
5209
|
}
|
|
5051
5210
|
}
|
|
5052
|
-
async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5211
|
+
async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
|
|
5053
5212
|
try {
|
|
5054
|
-
return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5213
|
+
return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed);
|
|
5055
5214
|
} catch (e) {
|
|
5056
5215
|
throw unconfirmed(e, pending.publicationId);
|
|
5057
5216
|
}
|
|
@@ -5066,8 +5225,8 @@ function unconfirmed(e, publicationId) {
|
|
|
5066
5225
|
details: { publicationId, cause: e.code }
|
|
5067
5226
|
});
|
|
5068
5227
|
}
|
|
5069
|
-
async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5070
|
-
const progress =
|
|
5228
|
+
async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
|
|
5229
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5071
5230
|
const beginBody = {
|
|
5072
5231
|
publicationId: pending.publicationId,
|
|
5073
5232
|
baseGeneration: pending.baseGeneration,
|
|
@@ -5078,32 +5237,32 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
|
5078
5237
|
let summary = null;
|
|
5079
5238
|
if (!pending.finalized) {
|
|
5080
5239
|
progress({ phase: "begin", message: "starting publication" });
|
|
5081
|
-
let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody,
|
|
5240
|
+
let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
|
|
5082
5241
|
summary = begin.publication;
|
|
5083
5242
|
if (begin.publication.state === "created") {
|
|
5084
5243
|
if (begin.upload && pending.pack) {
|
|
5085
5244
|
progress({ phase: "upload", message: "uploading pack", bytes: 0, total: pending.pack.size });
|
|
5086
|
-
let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size,
|
|
5245
|
+
let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
|
|
5087
5246
|
if (result === "expired") {
|
|
5088
|
-
begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody,
|
|
5247
|
+
begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
|
|
5089
5248
|
if (!begin.upload) throw new PublisherError("UPLOAD_MISSING", "server did not return fresh upload instructions");
|
|
5090
|
-
result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size,
|
|
5249
|
+
result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
|
|
5091
5250
|
if (result === "expired") throw new PublisherError("NETWORK", "upload authorization expired twice", { retryable: true });
|
|
5092
5251
|
}
|
|
5093
5252
|
}
|
|
5094
5253
|
progress({ phase: "finalize", message: "finalizing" });
|
|
5095
|
-
const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {},
|
|
5254
|
+
const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call2);
|
|
5096
5255
|
summary = fin.body.publication;
|
|
5097
5256
|
}
|
|
5098
5257
|
pending.finalized = true;
|
|
5099
5258
|
await store2.write(pending);
|
|
5100
5259
|
}
|
|
5101
5260
|
if (opts.wait === false) {
|
|
5102
|
-
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId,
|
|
5261
|
+
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call2);
|
|
5103
5262
|
return toOutcome(summary, resumed, pending.refUpdates);
|
|
5104
5263
|
}
|
|
5105
5264
|
progress({ phase: "validate", message: "waiting for validation" });
|
|
5106
|
-
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4,
|
|
5265
|
+
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call2);
|
|
5107
5266
|
if (summary.state === "accepted") {
|
|
5108
5267
|
await applyTrackingRefs(repoDir, remote, pending.refUpdates);
|
|
5109
5268
|
await store2.remove(pending.publicationId);
|
|
@@ -5114,23 +5273,23 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
|
5114
5273
|
}
|
|
5115
5274
|
return toOutcome(summary, resumed, pending.refUpdates);
|
|
5116
5275
|
}
|
|
5117
|
-
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs,
|
|
5276
|
+
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call2) {
|
|
5118
5277
|
const deadline = Date.now() + timeoutMs;
|
|
5119
5278
|
let delay2 = 500;
|
|
5120
5279
|
for (; ; ) {
|
|
5121
|
-
const s = await ctx.getStatus(repoId, publicationId, { ...
|
|
5280
|
+
const s = await ctx.getStatus(repoId, publicationId, { ...call2, waitSeconds: MAX_WAIT_SECONDS });
|
|
5122
5281
|
if (s.state === "accepted" || s.state === "rejected") return s;
|
|
5123
5282
|
if (Date.now() > deadline) {
|
|
5124
5283
|
throw new PublisherError("OUTCOME_UNKNOWN", `publication ${publicationId} has not completed yet; run status later`, { retryable: true, details: { publicationId } });
|
|
5125
5284
|
}
|
|
5126
|
-
await sleep(delay2,
|
|
5285
|
+
await sleep(delay2, call2.signal);
|
|
5127
5286
|
delay2 = Math.min(delay2 * 2, 5e3);
|
|
5128
5287
|
}
|
|
5129
5288
|
}
|
|
5130
|
-
async function resolvePending(ctx, repoDir, remote, pending,
|
|
5289
|
+
async function resolvePending(ctx, repoDir, remote, pending, call2, store2) {
|
|
5131
5290
|
let summary;
|
|
5132
5291
|
try {
|
|
5133
|
-
summary = await ctx.getStatus(pending.repoId, pending.publicationId,
|
|
5292
|
+
summary = await ctx.getStatus(pending.repoId, pending.publicationId, call2);
|
|
5134
5293
|
} catch (e) {
|
|
5135
5294
|
if (e instanceof PublisherError && e.code === "PUBLICATION_NOT_FOUND") {
|
|
5136
5295
|
await store2.remove(pending.publicationId);
|
|
@@ -5240,13 +5399,13 @@ async function writeManifest(gd, installed) {
|
|
|
5240
5399
|
await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
|
|
5241
5400
|
await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
|
|
5242
5401
|
}
|
|
5243
|
-
async function fetchRepository(ctx, repoId, opts,
|
|
5402
|
+
async function fetchRepository(ctx, repoId, opts, call2) {
|
|
5244
5403
|
const remote = opts.remote ?? "publisher";
|
|
5245
|
-
const progress =
|
|
5404
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5246
5405
|
progress({ phase: "fetch", message: "describing repository" });
|
|
5247
5406
|
const [, { body }, gd] = await Promise.all([
|
|
5248
5407
|
ensureRemote(opts.repoDir, remote, `${ctx.transport.endpoint}/v1/repos/${repoId}`),
|
|
5249
|
-
ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0,
|
|
5408
|
+
ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call2),
|
|
5250
5409
|
gitDir(opts.repoDir)
|
|
5251
5410
|
]);
|
|
5252
5411
|
const artifacts = [...body.base ? [{ kind: "bundle", ...body.base }] : [], ...body.packs.map((p) => ({ kind: "pack", ...p }))];
|
|
@@ -5257,14 +5416,14 @@ async function fetchRepository(ctx, repoId, opts, call) {
|
|
|
5257
5416
|
ctx,
|
|
5258
5417
|
gd,
|
|
5259
5418
|
artifacts.filter((a) => !installed.has(a.sha256)),
|
|
5260
|
-
|
|
5419
|
+
call2
|
|
5261
5420
|
);
|
|
5262
5421
|
if (!await haveAll(opts.repoDir, body.refs)) {
|
|
5263
5422
|
bytes += await install(
|
|
5264
5423
|
ctx,
|
|
5265
5424
|
gd,
|
|
5266
5425
|
artifacts.filter((a) => installed.has(a.sha256)),
|
|
5267
|
-
|
|
5426
|
+
call2
|
|
5268
5427
|
);
|
|
5269
5428
|
if (!await haveAll(opts.repoDir, body.refs)) throw new PublisherError("NETWORK", "the downloaded artifacts do not contain every advertised ref", { retryable: true });
|
|
5270
5429
|
}
|
|
@@ -5282,16 +5441,16 @@ async function haveAll(repoDir, refs) {
|
|
|
5282
5441
|
if (oids.size === 0) return true;
|
|
5283
5442
|
return (await batchCheck(repoDir, oids)).every((o) => o.type !== "missing");
|
|
5284
5443
|
}
|
|
5285
|
-
async function install(ctx, gd, artifacts,
|
|
5444
|
+
async function install(ctx, gd, artifacts, call2) {
|
|
5286
5445
|
if (artifacts.length === 0) return 0;
|
|
5287
|
-
const progress =
|
|
5446
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5288
5447
|
const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
|
|
5289
5448
|
let bytes = 0;
|
|
5290
5449
|
const total = artifacts.reduce((sum, a) => sum + a.size, 0);
|
|
5291
5450
|
try {
|
|
5292
5451
|
for (const a of artifacts) {
|
|
5293
5452
|
const file = (0, import_node_path6.join)(tmp, `${a.sha256}.${a.kind}`);
|
|
5294
|
-
bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size,
|
|
5453
|
+
bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call2);
|
|
5295
5454
|
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
|
|
5296
5455
|
if (a.kind === "bundle") {
|
|
5297
5456
|
await bundleUnbundleObjects(gd, file);
|
|
@@ -5322,10 +5481,10 @@ async function updateRefs(repoDir, remote, refs) {
|
|
|
5322
5481
|
await refTransaction(repoDir, updates);
|
|
5323
5482
|
return tracking;
|
|
5324
5483
|
}
|
|
5325
|
-
async function download(fetchImpl, url2, dest, sha256, size,
|
|
5484
|
+
async function download(fetchImpl, url2, dest, sha256, size, call2) {
|
|
5326
5485
|
let res;
|
|
5327
5486
|
try {
|
|
5328
|
-
res = await fetchImpl(url2,
|
|
5487
|
+
res = await fetchImpl(url2, call2.signal ? { signal: call2.signal } : {});
|
|
5329
5488
|
} catch (e) {
|
|
5330
5489
|
throw new PublisherError("NETWORK", `download failed: ${e.message}`, { retryable: true, cause: e });
|
|
5331
5490
|
}
|
|
@@ -5336,7 +5495,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
5336
5495
|
transform(chunk, controller) {
|
|
5337
5496
|
n += chunk.byteLength;
|
|
5338
5497
|
hash.update(chunk);
|
|
5339
|
-
|
|
5498
|
+
call2.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
|
|
5340
5499
|
controller.enqueue(chunk);
|
|
5341
5500
|
}
|
|
5342
5501
|
});
|
|
@@ -5392,11 +5551,37 @@ var Publisher = class {
|
|
|
5392
5551
|
fetch: this.fetchImpl
|
|
5393
5552
|
});
|
|
5394
5553
|
}
|
|
5395
|
-
|
|
5396
|
-
|
|
5554
|
+
/**
|
|
5555
|
+
* The question the repository's current preparation waits on, and its answer, at the portal that
|
|
5556
|
+
* serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
|
|
5557
|
+
* The caller confirms the portal's `/config` lists `PREPARATION` first.
|
|
5558
|
+
*/
|
|
5559
|
+
/**
|
|
5560
|
+
* A new application and its repository, made by the portal that serves this endpoint — the only
|
|
5561
|
+
* way a repository is created on a deployed stage (ADR-0016 A1). The caller confirms the portal's
|
|
5562
|
+
* `/config` lists `APPLICATION_CREATION` first, as it does for the other terminal routes.
|
|
5563
|
+
*/
|
|
5564
|
+
async createApplication(input) {
|
|
5565
|
+
return createApplication(input, this.portalCall());
|
|
5566
|
+
}
|
|
5567
|
+
async readPreparation(input) {
|
|
5568
|
+
return readPreparation(input, this.portalCall());
|
|
5569
|
+
}
|
|
5570
|
+
async answerPreparation(input) {
|
|
5571
|
+
return answerPreparation(input, this.portalCall());
|
|
5572
|
+
}
|
|
5573
|
+
portalCall() {
|
|
5574
|
+
return {
|
|
5575
|
+
token: this.tokenFn,
|
|
5576
|
+
refresh: async () => !this.opts.token && refreshStoredCredential(this.endpoint, { fetch: this.fetchImpl }),
|
|
5577
|
+
fetch: this.fetchImpl
|
|
5578
|
+
};
|
|
5579
|
+
}
|
|
5580
|
+
async getCapabilities(call2 = {}) {
|
|
5581
|
+
const r = await this.transport.request("GET", "/v1/capabilities", void 0, call2);
|
|
5397
5582
|
return r.body;
|
|
5398
5583
|
}
|
|
5399
|
-
async createRepository(options = {},
|
|
5584
|
+
async createRepository(options = {}, call2 = {}) {
|
|
5400
5585
|
const body = {
|
|
5401
5586
|
creationId: options.creationId ?? newCreationId(),
|
|
5402
5587
|
name: options.name ?? "",
|
|
@@ -5404,29 +5589,29 @@ var Publisher = class {
|
|
|
5404
5589
|
objectFormat: "sha1",
|
|
5405
5590
|
grants: options.grants ?? []
|
|
5406
5591
|
};
|
|
5407
|
-
const r = await this.transport.request("POST", "/v1/repos", body,
|
|
5592
|
+
const r = await this.transport.request("POST", "/v1/repos", body, call2);
|
|
5408
5593
|
return r.body.repository;
|
|
5409
5594
|
}
|
|
5410
|
-
async getRepository(repoId,
|
|
5411
|
-
const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0,
|
|
5595
|
+
async getRepository(repoId, call2 = {}) {
|
|
5596
|
+
const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call2);
|
|
5412
5597
|
return r.body.repository;
|
|
5413
5598
|
}
|
|
5414
5599
|
/** One page of the repositories the caller created (ADR-0011). An authenticated route, unlike capabilities. */
|
|
5415
|
-
async listRepositories(options = {},
|
|
5600
|
+
async listRepositories(options = {}, call2 = {}) {
|
|
5416
5601
|
const q = new URLSearchParams();
|
|
5417
5602
|
if (options.state) q.set("state", options.state);
|
|
5418
5603
|
if (options.limit !== void 0) q.set("limit", String(options.limit));
|
|
5419
5604
|
if (options.cursor) q.set("cursor", options.cursor);
|
|
5420
5605
|
const qs = q.toString() ? `?${q.toString()}` : "";
|
|
5421
|
-
return (await this.transport.request("GET", `/v1/repos${qs}`, void 0,
|
|
5606
|
+
return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call2)).body;
|
|
5422
5607
|
}
|
|
5423
|
-
async getPublicationStatus(repoId, publicationId,
|
|
5424
|
-
const q =
|
|
5608
|
+
async getPublicationStatus(repoId, publicationId, call2 = {}) {
|
|
5609
|
+
const q = call2.waitSeconds ? `?wait=${Math.min(call2.waitSeconds, MAX_WAIT_SECONDS)}` : "";
|
|
5425
5610
|
const r = await this.transport.request(
|
|
5426
5611
|
"GET",
|
|
5427
5612
|
`/v1/repos/${encodeURIComponent(repoId)}/publications/${encodeURIComponent(publicationId)}${q}`,
|
|
5428
5613
|
void 0,
|
|
5429
|
-
|
|
5614
|
+
call2
|
|
5430
5615
|
);
|
|
5431
5616
|
return r.body.publication;
|
|
5432
5617
|
}
|
|
@@ -5453,32 +5638,32 @@ var Publisher = class {
|
|
|
5453
5638
|
return { repoId: a.repoId, remote: a.remote };
|
|
5454
5639
|
}
|
|
5455
5640
|
/** Publish existing local commits. Never modifies the worktree; safe to retry and resume. */
|
|
5456
|
-
async publish(options,
|
|
5641
|
+
async publish(options, call2 = {}) {
|
|
5457
5642
|
const { repoId, remote } = await this.resolveRepo(options.repoDir, options.repoId);
|
|
5458
|
-
const outcome = await publish(this.publishContext(), repoId, remote, options,
|
|
5643
|
+
const outcome = await publish(this.publishContext(), repoId, remote, options, call2);
|
|
5459
5644
|
await noteAcceptedPublication(options.repoDir, outcome);
|
|
5460
5645
|
return outcome;
|
|
5461
5646
|
}
|
|
5462
5647
|
// ---- workspaces (docs/workspace-parity.md) ------------------------------------------------
|
|
5463
5648
|
/** Create a NEW editable workspace (clone + mapping) for a published repository. */
|
|
5464
|
-
async checkout(options,
|
|
5465
|
-
return checkoutWorkspace(this, options,
|
|
5649
|
+
async checkout(options, call2 = {}) {
|
|
5650
|
+
return checkoutWorkspace(this, options, call2);
|
|
5466
5651
|
}
|
|
5467
5652
|
/** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
|
|
5468
|
-
async adoptWorkspace(directory,
|
|
5469
|
-
return adoptWorkspace(this, directory,
|
|
5653
|
+
async adoptWorkspace(directory, call2 = {}) {
|
|
5654
|
+
return adoptWorkspace(this, directory, call2);
|
|
5470
5655
|
}
|
|
5471
5656
|
/** Offline: the persisted workspace mapping, or null. */
|
|
5472
5657
|
async workspace(directory) {
|
|
5473
5658
|
return getWorkspaceMapping(directory);
|
|
5474
5659
|
}
|
|
5475
5660
|
/** Where the workspace stands relative to the server (decided on real objects). */
|
|
5476
|
-
async workspaceStatus(directory,
|
|
5477
|
-
return inspectWorkspace(this, directory,
|
|
5661
|
+
async workspaceStatus(directory, call2 = {}) {
|
|
5662
|
+
return inspectWorkspace(this, directory, call2);
|
|
5478
5663
|
}
|
|
5479
5664
|
/** Fetch and, with `apply`, fast-forward or merge cleanly; advances the base. */
|
|
5480
|
-
async sync(options,
|
|
5481
|
-
return syncWorkspace(this, options,
|
|
5665
|
+
async sync(options, call2 = {}) {
|
|
5666
|
+
return syncWorkspace(this, options, call2);
|
|
5482
5667
|
}
|
|
5483
5668
|
/**
|
|
5484
5669
|
* A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
|
|
@@ -5490,7 +5675,7 @@ var Publisher = class {
|
|
|
5490
5675
|
* a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
|
|
5491
5676
|
* the composition, and the only one.
|
|
5492
5677
|
*/
|
|
5493
|
-
async syncChanges(options,
|
|
5678
|
+
async syncChanges(options, call2 = {}) {
|
|
5494
5679
|
const dir = options.directory;
|
|
5495
5680
|
await this.resolveRepo(dir);
|
|
5496
5681
|
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
@@ -5499,7 +5684,7 @@ var Publisher = class {
|
|
|
5499
5684
|
if (mapped && mapped.branch !== branch)
|
|
5500
5685
|
throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
|
|
5501
5686
|
const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
|
|
5502
|
-
const reconciled = await syncWorkspace(this, { directory: dir, apply: true },
|
|
5687
|
+
const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call2);
|
|
5503
5688
|
const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
|
|
5504
5689
|
if (reconciled.after === "conflict") {
|
|
5505
5690
|
const remote = reconciled.before.remote?.oid ?? null;
|
|
@@ -5508,25 +5693,25 @@ var Publisher = class {
|
|
|
5508
5693
|
}
|
|
5509
5694
|
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
5510
5695
|
if (tip === null || tip === (reconciled.before.remote?.oid ?? null)) return { ...base, state: "up_to_date", published: null, marked: false };
|
|
5511
|
-
const published = await this.publish({ repoDir: dir },
|
|
5696
|
+
const published = await this.publish({ repoDir: dir }, call2);
|
|
5512
5697
|
return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
|
|
5513
5698
|
}
|
|
5514
5699
|
/** Resolve interrupted publications recorded locally without creating duplicates. */
|
|
5515
|
-
async resumePublications(repoDir,
|
|
5700
|
+
async resumePublications(repoDir, call2 = {}) {
|
|
5516
5701
|
const { remote } = await this.resolveRepo(repoDir);
|
|
5517
5702
|
const out = [];
|
|
5518
5703
|
const store2 = await openPending(repoDir);
|
|
5519
5704
|
for (const p of await store2.list()) {
|
|
5520
5705
|
if (p.endpoint !== this.endpoint) continue;
|
|
5521
5706
|
if (!p.finalized) {
|
|
5522
|
-
out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId },
|
|
5707
|
+
out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call2).catch((e) => rethrowUnlessResolvable(e, p)));
|
|
5523
5708
|
continue;
|
|
5524
5709
|
}
|
|
5525
|
-
const deadline = Date.now() + (
|
|
5526
|
-
let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...
|
|
5710
|
+
const deadline = Date.now() + (call2.waitTimeoutMs ?? 5 * 6e4);
|
|
5711
|
+
let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5527
5712
|
while (s.state === "finalizing" && Date.now() < deadline) {
|
|
5528
|
-
|
|
5529
|
-
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...
|
|
5713
|
+
call2.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
|
|
5714
|
+
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5530
5715
|
}
|
|
5531
5716
|
out.push(toOutcome(s, true, p.refUpdates));
|
|
5532
5717
|
}
|
|
@@ -5541,17 +5726,17 @@ var Publisher = class {
|
|
|
5541
5726
|
* branches. Every read is a presigned download of the repository's bundle and packs (ADR-0013);
|
|
5542
5727
|
* artifacts already installed are not downloaded again.
|
|
5543
5728
|
*/
|
|
5544
|
-
async fetch(options,
|
|
5729
|
+
async fetch(options, call2 = {}) {
|
|
5545
5730
|
const { repoId } = await this.resolveRepo(options.repoDir, options.repoId);
|
|
5546
|
-
return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options,
|
|
5731
|
+
return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call2);
|
|
5547
5732
|
}
|
|
5548
5733
|
publishContext() {
|
|
5549
5734
|
return {
|
|
5550
5735
|
transport: this.transport,
|
|
5551
5736
|
fetchImpl: this.fetchImpl,
|
|
5552
5737
|
clientInfo: this.clientInfo,
|
|
5553
|
-
getRepository: (repoId,
|
|
5554
|
-
getStatus: (repoId, publicationId,
|
|
5738
|
+
getRepository: (repoId, call2) => this.getRepository(repoId, call2),
|
|
5739
|
+
getStatus: (repoId, publicationId, call2) => this.getPublicationStatus(repoId, publicationId, call2)
|
|
5555
5740
|
};
|
|
5556
5741
|
}
|
|
5557
5742
|
};
|
|
@@ -5631,8 +5816,8 @@ async function deviceLogin(opts) {
|
|
|
5631
5816
|
if (typeof advertised !== "string" || advertised === "") throw new PublisherError("UNAVAILABLE", "this deployment does not offer device login; obtain a token another way");
|
|
5632
5817
|
const base = normalize(advertised);
|
|
5633
5818
|
assertCredentialEndpoint(base);
|
|
5634
|
-
const
|
|
5635
|
-
const started = await
|
|
5819
|
+
const post2 = (path, fields) => postForm(base, path, fields, doFetch);
|
|
5820
|
+
const started = await post2("/v1/auth/device", { client_id: opts.clientId ?? "gitpub-cli" });
|
|
5636
5821
|
if (started.status !== 200) throw new PublisherError("UNAVAILABLE", `starting the login failed (${started.status})`, { retryable: true });
|
|
5637
5822
|
const s = started.body;
|
|
5638
5823
|
const deviceCode = str(s.device_code);
|
|
@@ -5646,7 +5831,7 @@ async function deviceLogin(opts) {
|
|
|
5646
5831
|
for (; ; ) {
|
|
5647
5832
|
await sleep2(intervalMs);
|
|
5648
5833
|
if (now() >= deadline) throw new PublisherError("EXPIRED", "the login expired before it was approved");
|
|
5649
|
-
const polled = await
|
|
5834
|
+
const polled = await post2("/v1/auth/token", { grant_type: DEVICE_CODE_GRANT, device_code: deviceCode });
|
|
5650
5835
|
if (polled.status === 200) {
|
|
5651
5836
|
const token = str(polled.body["access_token"]);
|
|
5652
5837
|
if (!token) throw new PublisherError("UNAVAILABLE", "the login returned no credential");
|
|
@@ -5693,6 +5878,21 @@ async function discoverEndpoint(portal, fetchImpl = fetch) {
|
|
|
5693
5878
|
return endpoint;
|
|
5694
5879
|
}
|
|
5695
5880
|
|
|
5881
|
+
// src/init.ts
|
|
5882
|
+
async function repositoryForInit(p, endpoint, name, opts = {}) {
|
|
5883
|
+
const portal = portalForEndpoint(endpoint);
|
|
5884
|
+
if (portal === null) return { repoId: (await p.createRepository({ name, grants: [] })).repoId, appId: null };
|
|
5885
|
+
const confirmed = await confirmPortal(portal, endpoint, opts.fetch);
|
|
5886
|
+
if (confirmed === null) {
|
|
5887
|
+
throw new PublisherError("UNAVAILABLE", `could not confirm that ${portal} publishes to ${endpoint}; run init again`, { retryable: true });
|
|
5888
|
+
}
|
|
5889
|
+
if (!confirmed.terminal.includes(APPLICATION_CREATION)) {
|
|
5890
|
+
throw new PublisherError("UNAVAILABLE", `${portal} cannot make an application from a terminal yet, and a repository needs one; ask whoever runs it to update the portal`);
|
|
5891
|
+
}
|
|
5892
|
+
const made = await p.createApplication({ portal, name });
|
|
5893
|
+
return { repoId: made.repository.repoId, appId: made.appId };
|
|
5894
|
+
}
|
|
5895
|
+
|
|
5696
5896
|
// src/login.ts
|
|
5697
5897
|
async function verifiedCapabilities(publisher) {
|
|
5698
5898
|
const caps = await publisher.getCapabilities();
|
|
@@ -5717,7 +5917,7 @@ function resolveChoice(answer, names) {
|
|
|
5717
5917
|
}
|
|
5718
5918
|
return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
|
|
5719
5919
|
}
|
|
5720
|
-
async function
|
|
5920
|
+
async function ask3(question) {
|
|
5721
5921
|
const rl = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
|
|
5722
5922
|
try {
|
|
5723
5923
|
return await rl.question(question);
|
|
@@ -5735,7 +5935,7 @@ ${purpose}
|
|
|
5735
5935
|
names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
|
|
5736
5936
|
`));
|
|
5737
5937
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5738
|
-
const answer = await
|
|
5938
|
+
const answer = await ask3(`
|
|
5739
5939
|
Which one? [1-${names.length}, or a name] `);
|
|
5740
5940
|
if (answer === null) return null;
|
|
5741
5941
|
const chosen = resolveChoice(answer, names);
|
|
@@ -5751,7 +5951,7 @@ Name this connection, so you never have to type ${endpoint} again.
|
|
|
5751
5951
|
(for example: prod, dev, staging)
|
|
5752
5952
|
`);
|
|
5753
5953
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5754
|
-
const answer = await
|
|
5954
|
+
const answer = await ask3(suggested ? `
|
|
5755
5955
|
Profile name [${suggested}]: ` : `
|
|
5756
5956
|
Profile name: `);
|
|
5757
5957
|
if (answer === null) return null;
|
|
@@ -5874,8 +6074,9 @@ async function followPublication(p, endpoint, o, opts) {
|
|
|
5874
6074
|
{ portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
|
|
5875
6075
|
{ onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
|
|
5876
6076
|
);
|
|
5877
|
-
const
|
|
5878
|
-
|
|
6077
|
+
const answerable = confirmed.terminal.includes(PREPARATION);
|
|
6078
|
+
const next = outcome.kind === "timed-out" ? { action: "follow", command: `publisher follow --publication ${o.publicationId} --json` } : outcome.kind === "waiting" && answerable ? { action: "question", command: "publisher question --json" } : void 0;
|
|
6079
|
+
return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
|
|
5879
6080
|
}
|
|
5880
6081
|
function followTimeoutMs(flag) {
|
|
5881
6082
|
if (flag === void 0) return FOLLOW_TIMEOUT_MS;
|
|
@@ -5888,8 +6089,10 @@ function stageLine(e, json) {
|
|
|
5888
6089
|
function inPortal(portal, lead, verb) {
|
|
5889
6090
|
return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
|
|
5890
6091
|
}
|
|
5891
|
-
function progressHint(r, portal) {
|
|
6092
|
+
function progressHint(r, portal, answerable = false) {
|
|
5892
6093
|
switch (r.kind) {
|
|
6094
|
+
case "waiting":
|
|
6095
|
+
return answerable ? "It is waiting for an answer from you. Run publisher question to see it." : inPortal(portal, "It is waiting for an answer from you.", "Answer it");
|
|
5893
6096
|
case "settled":
|
|
5894
6097
|
switch (r.ending) {
|
|
5895
6098
|
case "live":
|
|
@@ -5915,6 +6118,78 @@ function progressHint(r, portal) {
|
|
|
5915
6118
|
}
|
|
5916
6119
|
}
|
|
5917
6120
|
|
|
6121
|
+
// src/question.ts
|
|
6122
|
+
async function questionAt(p, endpoint, repoId, opts = {}) {
|
|
6123
|
+
const at = await portalAt(endpoint, opts);
|
|
6124
|
+
if ("reason" in at) return at;
|
|
6125
|
+
return shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6126
|
+
}
|
|
6127
|
+
async function answerAt(p, endpoint, repoId, specs, opts = {}) {
|
|
6128
|
+
const answers = parseSpecs(specs);
|
|
6129
|
+
const at = await portalAt(endpoint, opts);
|
|
6130
|
+
if ("reason" in at) return at;
|
|
6131
|
+
const read2 = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6132
|
+
if (read2.kind !== "view") return read2;
|
|
6133
|
+
if (read2.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read2.preparation.status})`);
|
|
6134
|
+
for (const [id, value] of Object.entries(answers)) {
|
|
6135
|
+
const question = read2.questions.find((q) => q.id === id);
|
|
6136
|
+
if (question === void 0) {
|
|
6137
|
+
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read2.questions.map((q) => q.id).join(", ")}`, {
|
|
6138
|
+
details: { questions: read2.questions }
|
|
6139
|
+
});
|
|
6140
|
+
}
|
|
6141
|
+
const values = question.options.map((o) => o.value);
|
|
6142
|
+
if (!values.includes(value)) {
|
|
6143
|
+
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read2.questions } });
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read2.preparation.id, revision: read2.preparation.revision, answers }));
|
|
6147
|
+
}
|
|
6148
|
+
function parseSpecs(specs) {
|
|
6149
|
+
if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
|
|
6150
|
+
const answers = {};
|
|
6151
|
+
for (const spec of specs) {
|
|
6152
|
+
const at = spec.indexOf("=");
|
|
6153
|
+
if (at <= 0 || at === spec.length - 1) throw new PublisherError("INVALID_REQUEST", `"${spec}" is not question=value`);
|
|
6154
|
+
answers[spec.slice(0, at)] = spec.slice(at + 1);
|
|
6155
|
+
}
|
|
6156
|
+
return answers;
|
|
6157
|
+
}
|
|
6158
|
+
async function portalAt(endpoint, opts) {
|
|
6159
|
+
const derived = portalForEndpoint(endpoint);
|
|
6160
|
+
const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
|
|
6161
|
+
if (derived === null || confirmed === null) return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
|
|
6162
|
+
if (!confirmed.terminal.includes(PREPARATION)) {
|
|
6163
|
+
return { kind: "unavailable", reason: "no-route", portal: derived, hint: `This portal cannot show the question in the terminal yet. Answer it in the portal: ${derived}.` };
|
|
6164
|
+
}
|
|
6165
|
+
return { portal: derived };
|
|
6166
|
+
}
|
|
6167
|
+
function shown(portal, r) {
|
|
6168
|
+
switch (r.kind) {
|
|
6169
|
+
case "view": {
|
|
6170
|
+
const { questions, ...preparation } = r.view;
|
|
6171
|
+
return { kind: "view", portal, preparation, questions, hint: hintFor(r.view) };
|
|
6172
|
+
}
|
|
6173
|
+
case "no-application":
|
|
6174
|
+
throw new PublisherError("NOT_FOUND", "this repository has no application in your organisation yet; publish it first");
|
|
6175
|
+
case "moved-on":
|
|
6176
|
+
throw new PublisherError("UNAVAILABLE", "the preparation moved on before the answer arrived; run publisher question again", { retryable: true });
|
|
6177
|
+
case "unauthorized":
|
|
6178
|
+
throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
|
|
6179
|
+
case "forbidden":
|
|
6180
|
+
throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in answer for this organisation (an owner or admin can)");
|
|
6181
|
+
case "unavailable":
|
|
6182
|
+
throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${r.detail}`, { retryable: true });
|
|
6183
|
+
case "malformed":
|
|
6184
|
+
throw new PublisherError("UNAVAILABLE", `the portal's answer could not be read: ${r.detail}`);
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
function hintFor(view) {
|
|
6188
|
+
if (view.questions.length === 0) return "Nothing is waiting for an answer.";
|
|
6189
|
+
const how = view.questions.map((q) => `${q.id}=<${q.options.map((o) => o.value).join("|")}>`).join(" ");
|
|
6190
|
+
return `It is waiting for your answer. Run: publisher answer ${how}`;
|
|
6191
|
+
}
|
|
6192
|
+
|
|
5918
6193
|
// src/main.ts
|
|
5919
6194
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
5920
6195
|
|
|
@@ -5938,7 +6213,8 @@ Usage:
|
|
|
5938
6213
|
publisher fetch [dir]
|
|
5939
6214
|
publisher checkout <dir> --repo <id> [--branch <name>] create a NEW editable workspace (never overlays a folder)
|
|
5940
6215
|
publisher workspace-status [dir] offline mapping + where the workspace stands (real three-way check)
|
|
5941
|
-
publisher sync [dir] [--message <text>]
|
|
6216
|
+
publisher sync [dir] [--message <text>] [--name <app>] THE way to publish: a folder linked to nothing first gets its
|
|
6217
|
+
application and repository (what init does); then it records the
|
|
5942
6218
|
changes (never .env, keys or node_modules), brings in what
|
|
5943
6219
|
was published meanwhile (fast-forward or a clean merge,
|
|
5944
6220
|
never a rewrite) and publishes. Stops on a conflict with
|
|
@@ -5951,6 +6227,10 @@ Usage:
|
|
|
5951
6227
|
result's next.command picks it up
|
|
5952
6228
|
publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
|
|
5953
6229
|
[--timeout <seconds>]
|
|
6230
|
+
publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
|
|
6231
|
+
with each option's value (answering needs owner or admin)
|
|
6232
|
+
publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
|
|
6233
|
+
the question's options is refused before anything is sent
|
|
5954
6234
|
publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
|
|
5955
6235
|
publisher info [dir] repository state as seen by the server
|
|
5956
6236
|
publisher doctor environment check (node, git, credentials, association)
|
|
@@ -6027,6 +6307,31 @@ async function main(argv) {
|
|
|
6027
6307
|
const token = str2(flags["token"]);
|
|
6028
6308
|
return { endpoint, p: createPublisher({ endpoint, endpointSource: source, ...token ? { token } : {}, clientName: "git-publisher-cli", clientVersion: CLIENT_VERSION }) };
|
|
6029
6309
|
};
|
|
6310
|
+
const linkFolder = async (p, endpoint, dir, attach) => {
|
|
6311
|
+
let onboarding = null;
|
|
6312
|
+
if (!await isGitRepo(dir) || !await headCommit(dir)) {
|
|
6313
|
+
onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
|
|
6314
|
+
log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
|
|
6315
|
+
}
|
|
6316
|
+
let repoId = attach.repoId ?? null;
|
|
6317
|
+
let appId = null;
|
|
6318
|
+
if (repoId === null) {
|
|
6319
|
+
const made = await repositoryForInit(p, endpoint, str2(flags["name"]) ?? dir.split("/").pop() ?? "");
|
|
6320
|
+
repoId = made.repoId;
|
|
6321
|
+
appId = made.appId;
|
|
6322
|
+
} else {
|
|
6323
|
+
await p.getRepository(repoId);
|
|
6324
|
+
}
|
|
6325
|
+
await p.associate(dir, repoId);
|
|
6326
|
+
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
6327
|
+
return {
|
|
6328
|
+
repoId,
|
|
6329
|
+
appId,
|
|
6330
|
+
created: attach.repoId === void 0,
|
|
6331
|
+
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6332
|
+
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
6333
|
+
};
|
|
6334
|
+
};
|
|
6030
6335
|
if (flags["version"] === true) {
|
|
6031
6336
|
process.stdout.write(`${packageVersion()}
|
|
6032
6337
|
`);
|
|
@@ -6160,33 +6465,9 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6160
6465
|
case "init": {
|
|
6161
6466
|
const dir = dirArg(1);
|
|
6162
6467
|
const { p, endpoint } = await client();
|
|
6163
|
-
const
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
onboarding = await onboardFolder({ dir, ...optStr(flags, "message") });
|
|
6167
|
-
log(`prepared ${onboarding.committed.length} files (${onboarding.excluded.length} excluded)`);
|
|
6168
|
-
}
|
|
6169
|
-
let repoId = str2(flags["repo"]);
|
|
6170
|
-
let created = false;
|
|
6171
|
-
if (!repoId) {
|
|
6172
|
-
const grants = [];
|
|
6173
|
-
const repo = await p.createRepository({ name: str2(flags["name"]) ?? dir.split("/").pop() ?? "", grants });
|
|
6174
|
-
repoId = repo.repoId;
|
|
6175
|
-
created = true;
|
|
6176
|
-
} else {
|
|
6177
|
-
await p.getRepository(repoId);
|
|
6178
|
-
}
|
|
6179
|
-
await p.associate(dir, repoId);
|
|
6180
|
-
const workspace = await p.adoptWorkspace(dir).catch(() => null);
|
|
6181
|
-
out({
|
|
6182
|
-
ok: true,
|
|
6183
|
-
repoId,
|
|
6184
|
-
created,
|
|
6185
|
-
endpoint,
|
|
6186
|
-
dir,
|
|
6187
|
-
onboarding: onboarding ? { commit: onboarding.commit, committed: onboarding.committed.length, excluded: onboarding.excluded } : null,
|
|
6188
|
-
workspace: workspace === null ? null : { branch: workspace.branch, baseOid: workspace.baseOid, baseGeneration: workspace.baseGeneration }
|
|
6189
|
-
});
|
|
6468
|
+
const repo = str2(flags["repo"]);
|
|
6469
|
+
const linked = await linkFolder(p, endpoint, dir, repo ? { repoId: repo } : {});
|
|
6470
|
+
out({ ok: true, ...linked, endpoint, dir });
|
|
6190
6471
|
return 0;
|
|
6191
6472
|
}
|
|
6192
6473
|
case "publish": {
|
|
@@ -6279,12 +6560,14 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6279
6560
|
const dir = dirArg(1);
|
|
6280
6561
|
const timeoutMs = followTimeoutMs(str2(flags["timeout"]));
|
|
6281
6562
|
const { p, endpoint } = await client(dir);
|
|
6563
|
+
const linked = await isGitRepo(dir) && await readAssociation(dir) !== null ? null : await linkFolder(p, endpoint, dir, {});
|
|
6282
6564
|
const r = await p.syncChanges({ directory: dir, markConflicts: flags["mark-conflicts"] === true, ...optStr(flags, "message") }, { onProgress: progress });
|
|
6283
6565
|
const o = r.published;
|
|
6284
6566
|
const followed = o !== null && o.state === "accepted" && flags["no-wait"] !== true ? await followPublication(p, endpoint, o, { onStage: emitStage, log, timeoutMs }) : null;
|
|
6285
6567
|
out({
|
|
6286
6568
|
ok: r.state === "published" || r.state === "up_to_date",
|
|
6287
6569
|
state: r.state,
|
|
6570
|
+
linked: linked === null ? null : { repoId: linked.repoId, appId: linked.appId, created: linked.created },
|
|
6288
6571
|
recorded: r.recorded,
|
|
6289
6572
|
merged: r.reconciled.applied,
|
|
6290
6573
|
conflicts: r.reconciled.before.conflicts,
|
|
@@ -6322,6 +6605,27 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6322
6605
|
out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
|
|
6323
6606
|
return 0;
|
|
6324
6607
|
}
|
|
6608
|
+
/**
|
|
6609
|
+
* The business question the app's current preparation waits on, from this folder's repository.
|
|
6610
|
+
* Exit 0 whether or not one is pending: `questions` says which. An outage is exit 3, never "none".
|
|
6611
|
+
*/
|
|
6612
|
+
case "question": {
|
|
6613
|
+
const dir = dirArg(1);
|
|
6614
|
+
const { p, endpoint } = await client(dir);
|
|
6615
|
+
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6616
|
+
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
6617
|
+
out({ repoId, ...await questionAt(p, endpoint, repoId) });
|
|
6618
|
+
return 0;
|
|
6619
|
+
}
|
|
6620
|
+
/** Answer it, as `question=value` pairs, re-reading the preparation first. */
|
|
6621
|
+
case "answer": {
|
|
6622
|
+
const dir = (0, import_node_path8.resolve)(".");
|
|
6623
|
+
const { p, endpoint } = await client(dir);
|
|
6624
|
+
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6625
|
+
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
6626
|
+
out({ repoId, ...await answerAt(p, endpoint, repoId, cmd.slice(1)) });
|
|
6627
|
+
return 0;
|
|
6628
|
+
}
|
|
6325
6629
|
case "skills": {
|
|
6326
6630
|
if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
|
|
6327
6631
|
const r = await installSkills({ log });
|