@minnovate/shadow-publisher 0.6.3 → 0.7.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.7.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"]
|
|
@@ -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
|
@@ -2809,6 +2809,12 @@ var PrincipalRecord = object({
|
|
|
2809
2809
|
principal: PrincipalId,
|
|
2810
2810
|
disabled: boolean2(),
|
|
2811
2811
|
disabledAt: _default(nullable(IsoDate), null),
|
|
2812
|
+
/**
|
|
2813
|
+
* When the credentials tenant made the change this record reflects (ADR-0017), and what a later
|
|
2814
|
+
* event is ordered against: EventBridge does not order deliveries, and a late enable must not undo
|
|
2815
|
+
* a newer disable. Null on a record written by hand, which any event supersedes.
|
|
2816
|
+
*/
|
|
2817
|
+
changedAt: _default(nullable(IsoDate), null),
|
|
2812
2818
|
note: _default(nullable(string2().check(_maxLength(256))), null)
|
|
2813
2819
|
});
|
|
2814
2820
|
|
|
@@ -3585,12 +3591,12 @@ async function resolveRevs(dir, revs, opts = {}) {
|
|
|
3585
3591
|
const out = revs.map(
|
|
3586
3592
|
(r) => r.startsWith("-") ? null : r.includes("\0") ? new PublisherError("INVALID_REQUEST", "NUL in git argument") : null
|
|
3587
3593
|
);
|
|
3588
|
-
const
|
|
3589
|
-
if (
|
|
3590
|
-
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input:
|
|
3594
|
+
const ask4 = revs.flatMap((r, i) => out[i] === null && !r.startsWith("-") ? [i] : []);
|
|
3595
|
+
if (ask4.length === 0) return out;
|
|
3596
|
+
const { stdout } = await runGit(["cat-file", "--batch-check=%(objectname)", "-Z"], { cwd: dir, input: ask4.map((i) => revs[i]).join("\0") + "\0", ...opts });
|
|
3591
3597
|
const records = stdout.toString("utf8").split("\0");
|
|
3592
3598
|
const misses = [];
|
|
3593
|
-
for (const [j, i] of
|
|
3599
|
+
for (const [j, i] of ask4.entries()) {
|
|
3594
3600
|
const record2 = records[j] ?? "";
|
|
3595
3601
|
if (record2 && !/ (missing|ambiguous)$/.test(record2)) out[i] = record2;
|
|
3596
3602
|
else if (!revs[i].includes("^{")) misses.push(i);
|
|
@@ -3990,7 +3996,7 @@ async function noteAcceptedPublication(directory, outcome, now = /* @__PURE__ */
|
|
|
3990
3996
|
if (!oid) return;
|
|
3991
3997
|
await advanceWorkspaceBase(directory, { oid, generation: outcome.resultGeneration }, now);
|
|
3992
3998
|
}
|
|
3993
|
-
async function adoptWorkspace(client, directory,
|
|
3999
|
+
async function adoptWorkspace(client, directory, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
3994
4000
|
const dir = (0, import_node_path3.resolve)(directory);
|
|
3995
4001
|
const existing = await getWorkspaceMapping(dir);
|
|
3996
4002
|
if (existing) return existing;
|
|
@@ -3999,13 +4005,13 @@ async function adoptWorkspace(client, directory, call = {}, now = /* @__PURE__ *
|
|
|
3999
4005
|
if (!a) throw new PublisherError("LOCAL_STATE", "this folder is not associated with a published repository; run init first");
|
|
4000
4006
|
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
4001
4007
|
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,
|
|
4008
|
+
const repo = await client.getRepository(a.repoId, call2);
|
|
4003
4009
|
const remoteOid = repo.refs[`refs/heads/${branch}`] ?? null;
|
|
4004
4010
|
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
4005
4011
|
let baseOid = null;
|
|
4006
4012
|
let baseGeneration = 0;
|
|
4007
4013
|
if (remoteOid) {
|
|
4008
|
-
if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote },
|
|
4014
|
+
if (!await objectExists(dir, remoteOid)) await client.fetch({ repoDir: dir, repoId: a.repoId, remote: a.remote }, call2);
|
|
4009
4015
|
const haveRemote = await objectExists(dir, remoteOid);
|
|
4010
4016
|
if (!tip) {
|
|
4011
4017
|
baseOid = haveRemote ? remoteOid : null;
|
|
@@ -4064,15 +4070,15 @@ async function removeStalePartials(target, repoId) {
|
|
|
4064
4070
|
}
|
|
4065
4071
|
return removed;
|
|
4066
4072
|
}
|
|
4067
|
-
async function checkoutWorkspace(client, opts,
|
|
4073
|
+
async function checkoutWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
4068
4074
|
const target = (0, import_node_path3.resolve)(opts.directory);
|
|
4069
4075
|
const remote = opts.remote ?? "publisher";
|
|
4070
4076
|
const empty2 = await isEmptyDir(target);
|
|
4071
4077
|
if (empty2 === false) throw new PublisherError("LOCAL_STATE", `${target} exists and is not empty; checkout never overlays an existing folder`);
|
|
4072
4078
|
await (0, import_promises4.mkdir)((0, import_node_path3.dirname)(target), { recursive: true });
|
|
4073
4079
|
const removed = await removeStalePartials(target, opts.repoId);
|
|
4074
|
-
if (removed.length)
|
|
4075
|
-
const repo = await client.getRepository(opts.repoId,
|
|
4080
|
+
if (removed.length) call2.onProgress?.({ phase: "checkout", message: `removed ${removed.length} interrupted checkout(s)` });
|
|
4081
|
+
const repo = await client.getRepository(opts.repoId, call2);
|
|
4076
4082
|
const branch = opts.branch ?? repo.head.symbolic.replace(/^refs\/heads\//, "");
|
|
4077
4083
|
const tip = repo.refs[`refs/heads/${branch}`] ?? null;
|
|
4078
4084
|
if (!tip && Object.keys(repo.refs).length > 0) throw new PublisherError("LOCAL_STATE", `branch ${branch} does not exist on the server`);
|
|
@@ -4083,7 +4089,7 @@ async function checkoutWorkspace(client, opts, call = {}, now = /* @__PURE__ */
|
|
|
4083
4089
|
await runGit(["init", "-q", `--initial-branch=${branch}`, partial2]);
|
|
4084
4090
|
await writeAssociation(partial2, { endpoint: client.endpoint, repoId: opts.repoId, remote });
|
|
4085
4091
|
if (tip) {
|
|
4086
|
-
await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote },
|
|
4092
|
+
await client.fetch({ repoDir: partial2, repoId: opts.repoId, remote }, call2);
|
|
4087
4093
|
const tracking = `refs/remotes/${remote}/${branch}`;
|
|
4088
4094
|
const have = await revParse(partial2, tracking);
|
|
4089
4095
|
if (have !== tip) throw new PublisherError("LOCAL_STATE", `fetched ${tracking} is ${have ?? "missing"}, expected ${tip}; retry the checkout`);
|
|
@@ -4121,7 +4127,7 @@ async function countCommits(dir, range) {
|
|
|
4121
4127
|
const r = await runGit(["rev-list", "--count", range], { cwd: dir, allowFailure: true });
|
|
4122
4128
|
return r.code === 0 ? Number(r.stdout.toString("utf8").trim()) : 0;
|
|
4123
4129
|
}
|
|
4124
|
-
async function inspectWorkspace(client, directory,
|
|
4130
|
+
async function inspectWorkspace(client, directory, call2 = {}) {
|
|
4125
4131
|
const dir = (0, import_node_path3.resolve)(directory);
|
|
4126
4132
|
const mapping = await getWorkspaceMapping(dir);
|
|
4127
4133
|
const pending = await listPending(dir).catch(() => []);
|
|
@@ -4144,7 +4150,7 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4144
4150
|
const hasLocalWork = dirty || untrackedPaths.length > 0;
|
|
4145
4151
|
const checkedOut = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
4146
4152
|
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,
|
|
4153
|
+
const repo = await client.getRepository(mapping.repoId, call2);
|
|
4148
4154
|
const remoteOid = repo.refs[branchRef] ?? null;
|
|
4149
4155
|
const remote = { oid: remoteOid, generation: repo.generation };
|
|
4150
4156
|
if (mapping.baseOid && tip) local.commitsAheadOfBase = await countCommits(dir, `${mapping.baseOid}..${tip}`);
|
|
@@ -4162,7 +4168,7 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4162
4168
|
}
|
|
4163
4169
|
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
4170
|
if (!await objectExists(dir, remoteOid)) {
|
|
4165
|
-
await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote },
|
|
4171
|
+
await client.fetch({ repoDir: dir, repoId: mapping.repoId, remote: mapping.remote }, call2);
|
|
4166
4172
|
if (!await objectExists(dir, remoteOid)) return done("remote_ahead", "server tip could not be fetched; retry");
|
|
4167
4173
|
}
|
|
4168
4174
|
if (remoteOid === tip)
|
|
@@ -4189,10 +4195,10 @@ async function inspectWorkspace(client, directory, call = {}) {
|
|
|
4189
4195
|
merge2.conflicts.map((c) => c.path)
|
|
4190
4196
|
);
|
|
4191
4197
|
}
|
|
4192
|
-
async function syncWorkspace(client, opts,
|
|
4198
|
+
async function syncWorkspace(client, opts, call2 = {}, now = /* @__PURE__ */ new Date()) {
|
|
4193
4199
|
const dir = (0, import_node_path3.resolve)(opts.directory);
|
|
4194
|
-
await adoptWorkspace(client, dir,
|
|
4195
|
-
const before = await inspectWorkspace(client, dir,
|
|
4200
|
+
await adoptWorkspace(client, dir, call2, now);
|
|
4201
|
+
const before = await inspectWorkspace(client, dir, call2);
|
|
4196
4202
|
if (!before.mapping) throw new PublisherError("LOCAL_STATE", "not a workspace; run checkout");
|
|
4197
4203
|
const mapping = before.mapping;
|
|
4198
4204
|
const branchRef = `refs/heads/${mapping.branch}`;
|
|
@@ -4216,7 +4222,7 @@ async function syncWorkspace(client, opts, call = {}, now = /* @__PURE__ */ new
|
|
|
4216
4222
|
updated = await advanceWorkspaceBase(dir, { oid: before.remote.oid, generation: before.remote.generation }, now);
|
|
4217
4223
|
}
|
|
4218
4224
|
}
|
|
4219
|
-
const after = applied ? (await inspectWorkspace(client, dir,
|
|
4225
|
+
const after = applied ? (await inspectWorkspace(client, dir, call2)).state : before.state;
|
|
4220
4226
|
return { before, applied, mapping: updated, after };
|
|
4221
4227
|
}
|
|
4222
4228
|
|
|
@@ -4668,6 +4674,7 @@ async function followRun(input, opts) {
|
|
|
4668
4674
|
seen.set(entry.stage, entry.state);
|
|
4669
4675
|
opts.onStage?.(entry);
|
|
4670
4676
|
}
|
|
4677
|
+
if (isRecord(run.run) && run.run["status"] === "awaiting-approval") return { kind: "waiting", stages: read.stages };
|
|
4671
4678
|
const end = ending(read.stages);
|
|
4672
4679
|
if (end !== null) return { kind: "settled", ending: end, failedAt: read.stages.find((s) => s.state === "failed")?.word ?? null, stages: read.stages };
|
|
4673
4680
|
}
|
|
@@ -4729,6 +4736,87 @@ function isRecord(value) {
|
|
|
4729
4736
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4730
4737
|
}
|
|
4731
4738
|
|
|
4739
|
+
// ../client/src/preparation.ts
|
|
4740
|
+
var PREPARATION = "preparation";
|
|
4741
|
+
var STATUSES = ["queued", "running", "needs-choice", "ready", "blocked", "cancelled"];
|
|
4742
|
+
function readPreparation(input, opts) {
|
|
4743
|
+
return call(input, opts, void 0);
|
|
4744
|
+
}
|
|
4745
|
+
function answerPreparation(input, opts) {
|
|
4746
|
+
return call(input, opts, { id: input.id, revision: input.revision, action: "answers", answers: input.answers });
|
|
4747
|
+
}
|
|
4748
|
+
async function call(input, opts, body) {
|
|
4749
|
+
const fetchImpl = opts.fetch ?? fetch;
|
|
4750
|
+
const url2 = `${input.portal.replace(/\/+$/, "")}/api/v1/terminal/repositories/${encodeURIComponent(input.repoId)}/preparation`;
|
|
4751
|
+
let refreshed = false;
|
|
4752
|
+
for (; ; ) {
|
|
4753
|
+
const answer = await ask2(fetchImpl, url2, await opts.token(), body);
|
|
4754
|
+
if (answer.status === 401 && !refreshed && opts.refresh !== void 0) {
|
|
4755
|
+
refreshed = true;
|
|
4756
|
+
if (await opts.refresh()) continue;
|
|
4757
|
+
}
|
|
4758
|
+
if (answer.status === 401) return { kind: "unauthorized" };
|
|
4759
|
+
if (answer.status === 403) return { kind: "forbidden" };
|
|
4760
|
+
if (answer.status === 409) return { kind: "moved-on" };
|
|
4761
|
+
if (answer.status === 404) {
|
|
4762
|
+
const code = isRecord2(answer.body) ? answer.body["error"] : void 0;
|
|
4763
|
+
return code === "no-such-application" ? { kind: "no-application" } : { kind: "unavailable", detail: "the portal does not serve this address" };
|
|
4764
|
+
}
|
|
4765
|
+
if (answer.status === 200) {
|
|
4766
|
+
const view = readView(answer.body);
|
|
4767
|
+
return "detail" in view ? { kind: "malformed", detail: view.detail } : { kind: "view", view };
|
|
4768
|
+
}
|
|
4769
|
+
return { kind: "unavailable", detail: answer.status === 0 ? "the portal could not be reached" : `the portal answered ${answer.status}` };
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
function readView(body) {
|
|
4773
|
+
if (!isRecord2(body)) return { detail: "an answer that is not a preparation" };
|
|
4774
|
+
const { id, revision, status, message, questions, publication } = body;
|
|
4775
|
+
if (typeof id !== "string" || id === "") return { detail: "a preparation with no id" };
|
|
4776
|
+
if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) return { detail: "a preparation with no revision" };
|
|
4777
|
+
if (!STATUSES.includes(status)) return { detail: "a preparation with an unknown status" };
|
|
4778
|
+
if (typeof message !== "string") return { detail: "a preparation with no message" };
|
|
4779
|
+
if (!Array.isArray(questions)) return { detail: "a preparation with no list of questions" };
|
|
4780
|
+
const read = [];
|
|
4781
|
+
for (const q of questions) {
|
|
4782
|
+
if (!isRecord2(q) || typeof q["id"] !== "string" || typeof q["text"] !== "string" || !Array.isArray(q["options"]) || q["options"].length === 0) {
|
|
4783
|
+
return { detail: "a question that cannot be answered" };
|
|
4784
|
+
}
|
|
4785
|
+
const options = [];
|
|
4786
|
+
for (const o of q["options"]) {
|
|
4787
|
+
if (!isRecord2(o) || typeof o["value"] !== "string" || typeof o["label"] !== "string") return { detail: "a question with an unreadable option" };
|
|
4788
|
+
options.push({ value: o["value"], label: o["label"] });
|
|
4789
|
+
}
|
|
4790
|
+
read.push({ id: q["id"], text: q["text"], options });
|
|
4791
|
+
}
|
|
4792
|
+
let pub = null;
|
|
4793
|
+
if (isRecord2(publication) && typeof publication["status"] === "string" && typeof publication["message"] === "string") {
|
|
4794
|
+
pub = { status: publication["status"], message: publication["message"], ...typeof publication["url"] === "string" ? { url: publication["url"] } : {} };
|
|
4795
|
+
}
|
|
4796
|
+
return { id, revision, status, message, questions: read, publication: pub };
|
|
4797
|
+
}
|
|
4798
|
+
async function ask2(fetchImpl, url2, token, body) {
|
|
4799
|
+
try {
|
|
4800
|
+
const res = await fetchImpl(url2, {
|
|
4801
|
+
method: body === void 0 ? "GET" : "POST",
|
|
4802
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json", ...body === void 0 ? {} : { "content-type": "application/json" } },
|
|
4803
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
4804
|
+
signal: AbortSignal.timeout(3e4)
|
|
4805
|
+
});
|
|
4806
|
+
const text = await res.text();
|
|
4807
|
+
try {
|
|
4808
|
+
return { status: res.status, body: text ? JSON.parse(text) : null };
|
|
4809
|
+
} catch {
|
|
4810
|
+
return { status: res.status, body: void 0 };
|
|
4811
|
+
}
|
|
4812
|
+
} catch {
|
|
4813
|
+
return { status: 0, body: null };
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
function isRecord2(value) {
|
|
4817
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4732
4820
|
// ../client/src/onboard.ts
|
|
4733
4821
|
var import_promises6 = require("node:fs/promises");
|
|
4734
4822
|
var import_node_path5 = require("node:path");
|
|
@@ -4968,13 +5056,13 @@ async function uploadPack(fetchImpl, upload, packPath, size, opts) {
|
|
|
4968
5056
|
}
|
|
4969
5057
|
|
|
4970
5058
|
// ../client/src/publish.ts
|
|
4971
|
-
async function publish(ctx, repoId, remote, opts,
|
|
4972
|
-
const progress =
|
|
5059
|
+
async function publish(ctx, repoId, remote, opts, call2) {
|
|
5060
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
4973
5061
|
const { repoDir } = opts;
|
|
4974
5062
|
const store2 = await openPending(repoDir);
|
|
4975
5063
|
const pendings = await store2.list();
|
|
4976
5064
|
progress({ phase: "resolve", message: "reading repository state" });
|
|
4977
|
-
const repo = await ctx.getRepository(repoId,
|
|
5065
|
+
const repo = await ctx.getRepository(repoId, call2);
|
|
4978
5066
|
const specs = opts.updates ?? [await defaultUpdate(repoDir)];
|
|
4979
5067
|
let refUpdates = [];
|
|
4980
5068
|
let nothingNew = null;
|
|
@@ -4990,9 +5078,9 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
4990
5078
|
const sameIntent = !nothingNew && pending2.baseGeneration === repo.generation && canonicalJson(refUpdates) === canonicalJson(pending2.refUpdates);
|
|
4991
5079
|
if (sameIntent && (pending2.pack === null || await packFileUsable(pending2))) {
|
|
4992
5080
|
progress({ phase: "resolve", message: `resuming publication ${pending2.publicationId}` });
|
|
4993
|
-
return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts,
|
|
5081
|
+
return runPublication(ctx, repoId, remote, repoDir, store2, pending2, opts, call2, true);
|
|
4994
5082
|
}
|
|
4995
|
-
const outcome = await resolvePending(ctx, repoDir, remote, pending2,
|
|
5083
|
+
const outcome = await resolvePending(ctx, repoDir, remote, pending2, call2, store2);
|
|
4996
5084
|
if (outcome.state === "finalizing" || outcome.state === "unknown") {
|
|
4997
5085
|
throw new PublisherError("OUTCOME_UNKNOWN", `publication ${pending2.publicationId} is still being processed; run status before publishing again`, {
|
|
4998
5086
|
retryable: true,
|
|
@@ -5000,7 +5088,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5000
5088
|
});
|
|
5001
5089
|
}
|
|
5002
5090
|
if (outcome.state === "accepted") {
|
|
5003
|
-
const fresh = await ctx.getRepository(repoId,
|
|
5091
|
+
const fresh = await ctx.getRepository(repoId, call2);
|
|
5004
5092
|
let remaining = [];
|
|
5005
5093
|
try {
|
|
5006
5094
|
remaining = await resolveUpdates(repoDir, fresh, specs);
|
|
@@ -5008,7 +5096,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5008
5096
|
if (!(e instanceof PublisherError && e.code === "LOCAL_STATE")) throw e;
|
|
5009
5097
|
}
|
|
5010
5098
|
if (remaining.length === 0) return toOutcome(outcome, true, pending2.refUpdates);
|
|
5011
|
-
return publish(ctx, repoId, remote, opts,
|
|
5099
|
+
return publish(ctx, repoId, remote, opts, call2);
|
|
5012
5100
|
}
|
|
5013
5101
|
}
|
|
5014
5102
|
if (nothingNew) throw nothingNew;
|
|
@@ -5022,7 +5110,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5022
5110
|
const exclude = [];
|
|
5023
5111
|
for (const info of await batchCheck(repoDir, remoteOids)) if (info.type !== "missing") exclude.push(info.oid);
|
|
5024
5112
|
const outPath = store2.packPath(publicationId);
|
|
5025
|
-
const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...
|
|
5113
|
+
const built = await buildPack(repoDir, { include: wants, exclude, outPath, ...call2.signal ? { signal: call2.signal } : {} });
|
|
5026
5114
|
pack = { sha256: built.sha256, size: built.size, path: outPath };
|
|
5027
5115
|
progress({ phase: "pack", message: `packed ${built.objectCount} objects (${built.size} bytes)`, bytes: built.size, total: built.size });
|
|
5028
5116
|
}
|
|
@@ -5038,7 +5126,7 @@ async function publish(ctx, repoId, remote, opts, call) {
|
|
|
5038
5126
|
finalized: false
|
|
5039
5127
|
};
|
|
5040
5128
|
await store2.write(pending);
|
|
5041
|
-
return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5129
|
+
return runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, false);
|
|
5042
5130
|
}
|
|
5043
5131
|
async function packFileUsable(p) {
|
|
5044
5132
|
if (!p.pack) return true;
|
|
@@ -5049,9 +5137,9 @@ async function packFileUsable(p) {
|
|
|
5049
5137
|
return false;
|
|
5050
5138
|
}
|
|
5051
5139
|
}
|
|
5052
|
-
async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5140
|
+
async function runPublication(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
|
|
5053
5141
|
try {
|
|
5054
|
-
return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5142
|
+
return await sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed);
|
|
5055
5143
|
} catch (e) {
|
|
5056
5144
|
throw unconfirmed(e, pending.publicationId);
|
|
5057
5145
|
}
|
|
@@ -5066,8 +5154,8 @@ function unconfirmed(e, publicationId) {
|
|
|
5066
5154
|
details: { publicationId, cause: e.code }
|
|
5067
5155
|
});
|
|
5068
5156
|
}
|
|
5069
|
-
async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
5070
|
-
const progress =
|
|
5157
|
+
async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts, call2, resumed) {
|
|
5158
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5071
5159
|
const beginBody = {
|
|
5072
5160
|
publicationId: pending.publicationId,
|
|
5073
5161
|
baseGeneration: pending.baseGeneration,
|
|
@@ -5078,32 +5166,32 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
|
5078
5166
|
let summary = null;
|
|
5079
5167
|
if (!pending.finalized) {
|
|
5080
5168
|
progress({ phase: "begin", message: "starting publication" });
|
|
5081
|
-
let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody,
|
|
5169
|
+
let begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
|
|
5082
5170
|
summary = begin.publication;
|
|
5083
5171
|
if (begin.publication.state === "created") {
|
|
5084
5172
|
if (begin.upload && pending.pack) {
|
|
5085
5173
|
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,
|
|
5174
|
+
let result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
|
|
5087
5175
|
if (result === "expired") {
|
|
5088
|
-
begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody,
|
|
5176
|
+
begin = (await ctx.transport.request("POST", `/v1/repos/${repoId}/publications`, beginBody, call2)).body;
|
|
5089
5177
|
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,
|
|
5178
|
+
result = await uploadPack(ctx.fetchImpl, begin.upload, pending.pack.path, pending.pack.size, call2);
|
|
5091
5179
|
if (result === "expired") throw new PublisherError("NETWORK", "upload authorization expired twice", { retryable: true });
|
|
5092
5180
|
}
|
|
5093
5181
|
}
|
|
5094
5182
|
progress({ phase: "finalize", message: "finalizing" });
|
|
5095
|
-
const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {},
|
|
5183
|
+
const fin = await ctx.transport.request("POST", `/v1/repos/${repoId}/publications/${pending.publicationId}/finalize`, {}, call2);
|
|
5096
5184
|
summary = fin.body.publication;
|
|
5097
5185
|
}
|
|
5098
5186
|
pending.finalized = true;
|
|
5099
5187
|
await store2.write(pending);
|
|
5100
5188
|
}
|
|
5101
5189
|
if (opts.wait === false) {
|
|
5102
|
-
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId,
|
|
5190
|
+
summary = summary ?? await ctx.getStatus(repoId, pending.publicationId, call2);
|
|
5103
5191
|
return toOutcome(summary, resumed, pending.refUpdates);
|
|
5104
5192
|
}
|
|
5105
5193
|
progress({ phase: "validate", message: "waiting for validation" });
|
|
5106
|
-
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4,
|
|
5194
|
+
summary = await waitForOutcome(ctx, repoId, pending.publicationId, opts.waitTimeoutMs ?? 10 * 6e4, call2);
|
|
5107
5195
|
if (summary.state === "accepted") {
|
|
5108
5196
|
await applyTrackingRefs(repoDir, remote, pending.refUpdates);
|
|
5109
5197
|
await store2.remove(pending.publicationId);
|
|
@@ -5114,23 +5202,23 @@ async function sendAndWait(ctx, repoId, remote, repoDir, store2, pending, opts,
|
|
|
5114
5202
|
}
|
|
5115
5203
|
return toOutcome(summary, resumed, pending.refUpdates);
|
|
5116
5204
|
}
|
|
5117
|
-
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs,
|
|
5205
|
+
async function waitForOutcome(ctx, repoId, publicationId, timeoutMs, call2) {
|
|
5118
5206
|
const deadline = Date.now() + timeoutMs;
|
|
5119
5207
|
let delay2 = 500;
|
|
5120
5208
|
for (; ; ) {
|
|
5121
|
-
const s = await ctx.getStatus(repoId, publicationId, { ...
|
|
5209
|
+
const s = await ctx.getStatus(repoId, publicationId, { ...call2, waitSeconds: MAX_WAIT_SECONDS });
|
|
5122
5210
|
if (s.state === "accepted" || s.state === "rejected") return s;
|
|
5123
5211
|
if (Date.now() > deadline) {
|
|
5124
5212
|
throw new PublisherError("OUTCOME_UNKNOWN", `publication ${publicationId} has not completed yet; run status later`, { retryable: true, details: { publicationId } });
|
|
5125
5213
|
}
|
|
5126
|
-
await sleep(delay2,
|
|
5214
|
+
await sleep(delay2, call2.signal);
|
|
5127
5215
|
delay2 = Math.min(delay2 * 2, 5e3);
|
|
5128
5216
|
}
|
|
5129
5217
|
}
|
|
5130
|
-
async function resolvePending(ctx, repoDir, remote, pending,
|
|
5218
|
+
async function resolvePending(ctx, repoDir, remote, pending, call2, store2) {
|
|
5131
5219
|
let summary;
|
|
5132
5220
|
try {
|
|
5133
|
-
summary = await ctx.getStatus(pending.repoId, pending.publicationId,
|
|
5221
|
+
summary = await ctx.getStatus(pending.repoId, pending.publicationId, call2);
|
|
5134
5222
|
} catch (e) {
|
|
5135
5223
|
if (e instanceof PublisherError && e.code === "PUBLICATION_NOT_FOUND") {
|
|
5136
5224
|
await store2.remove(pending.publicationId);
|
|
@@ -5240,13 +5328,13 @@ async function writeManifest(gd, installed) {
|
|
|
5240
5328
|
await (0, import_promises8.mkdir)((0, import_node_path6.join)(gd, "publisher"), { recursive: true });
|
|
5241
5329
|
await (0, import_promises8.writeFile)((0, import_node_path6.join)(gd, "publisher", "installed.json"), JSON.stringify(installed));
|
|
5242
5330
|
}
|
|
5243
|
-
async function fetchRepository(ctx, repoId, opts,
|
|
5331
|
+
async function fetchRepository(ctx, repoId, opts, call2) {
|
|
5244
5332
|
const remote = opts.remote ?? "publisher";
|
|
5245
|
-
const progress =
|
|
5333
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5246
5334
|
progress({ phase: "fetch", message: "describing repository" });
|
|
5247
5335
|
const [, { body }, gd] = await Promise.all([
|
|
5248
5336
|
ensureRemote(opts.repoDir, remote, `${ctx.transport.endpoint}/v1/repos/${repoId}`),
|
|
5249
|
-
ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0,
|
|
5337
|
+
ctx.transport.request("GET", `/v1/repos/${repoId}/bundle`, void 0, call2),
|
|
5250
5338
|
gitDir(opts.repoDir)
|
|
5251
5339
|
]);
|
|
5252
5340
|
const artifacts = [...body.base ? [{ kind: "bundle", ...body.base }] : [], ...body.packs.map((p) => ({ kind: "pack", ...p }))];
|
|
@@ -5257,14 +5345,14 @@ async function fetchRepository(ctx, repoId, opts, call) {
|
|
|
5257
5345
|
ctx,
|
|
5258
5346
|
gd,
|
|
5259
5347
|
artifacts.filter((a) => !installed.has(a.sha256)),
|
|
5260
|
-
|
|
5348
|
+
call2
|
|
5261
5349
|
);
|
|
5262
5350
|
if (!await haveAll(opts.repoDir, body.refs)) {
|
|
5263
5351
|
bytes += await install(
|
|
5264
5352
|
ctx,
|
|
5265
5353
|
gd,
|
|
5266
5354
|
artifacts.filter((a) => installed.has(a.sha256)),
|
|
5267
|
-
|
|
5355
|
+
call2
|
|
5268
5356
|
);
|
|
5269
5357
|
if (!await haveAll(opts.repoDir, body.refs)) throw new PublisherError("NETWORK", "the downloaded artifacts do not contain every advertised ref", { retryable: true });
|
|
5270
5358
|
}
|
|
@@ -5282,16 +5370,16 @@ async function haveAll(repoDir, refs) {
|
|
|
5282
5370
|
if (oids.size === 0) return true;
|
|
5283
5371
|
return (await batchCheck(repoDir, oids)).every((o) => o.type !== "missing");
|
|
5284
5372
|
}
|
|
5285
|
-
async function install(ctx, gd, artifacts,
|
|
5373
|
+
async function install(ctx, gd, artifacts, call2) {
|
|
5286
5374
|
if (artifacts.length === 0) return 0;
|
|
5287
|
-
const progress =
|
|
5375
|
+
const progress = call2.onProgress ?? (() => void 0);
|
|
5288
5376
|
const tmp = await (0, import_promises8.mkdtemp)((0, import_node_path6.join)((0, import_node_os3.tmpdir)(), "gitpub-fetch-"));
|
|
5289
5377
|
let bytes = 0;
|
|
5290
5378
|
const total = artifacts.reduce((sum, a) => sum + a.size, 0);
|
|
5291
5379
|
try {
|
|
5292
5380
|
for (const a of artifacts) {
|
|
5293
5381
|
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,
|
|
5382
|
+
bytes += await download(ctx.fetchImpl, a.url, file, a.sha256, a.size, call2);
|
|
5295
5383
|
progress({ phase: "fetch", message: `installing ${a.kind}`, bytes, total });
|
|
5296
5384
|
if (a.kind === "bundle") {
|
|
5297
5385
|
await bundleUnbundleObjects(gd, file);
|
|
@@ -5322,10 +5410,10 @@ async function updateRefs(repoDir, remote, refs) {
|
|
|
5322
5410
|
await refTransaction(repoDir, updates);
|
|
5323
5411
|
return tracking;
|
|
5324
5412
|
}
|
|
5325
|
-
async function download(fetchImpl, url2, dest, sha256, size,
|
|
5413
|
+
async function download(fetchImpl, url2, dest, sha256, size, call2) {
|
|
5326
5414
|
let res;
|
|
5327
5415
|
try {
|
|
5328
|
-
res = await fetchImpl(url2,
|
|
5416
|
+
res = await fetchImpl(url2, call2.signal ? { signal: call2.signal } : {});
|
|
5329
5417
|
} catch (e) {
|
|
5330
5418
|
throw new PublisherError("NETWORK", `download failed: ${e.message}`, { retryable: true, cause: e });
|
|
5331
5419
|
}
|
|
@@ -5336,7 +5424,7 @@ async function download(fetchImpl, url2, dest, sha256, size, call) {
|
|
|
5336
5424
|
transform(chunk, controller) {
|
|
5337
5425
|
n += chunk.byteLength;
|
|
5338
5426
|
hash.update(chunk);
|
|
5339
|
-
|
|
5427
|
+
call2.onProgress?.({ phase: "fetch", message: "downloading", bytes: n, total: size });
|
|
5340
5428
|
controller.enqueue(chunk);
|
|
5341
5429
|
}
|
|
5342
5430
|
});
|
|
@@ -5392,11 +5480,29 @@ var Publisher = class {
|
|
|
5392
5480
|
fetch: this.fetchImpl
|
|
5393
5481
|
});
|
|
5394
5482
|
}
|
|
5395
|
-
|
|
5396
|
-
|
|
5483
|
+
/**
|
|
5484
|
+
* The question the repository's current preparation waits on, and its answer, at the portal that
|
|
5485
|
+
* serves this endpoint — with this publisher's credential and its one refresh, as `followRun`.
|
|
5486
|
+
* The caller confirms the portal's `/config` lists `PREPARATION` first.
|
|
5487
|
+
*/
|
|
5488
|
+
async readPreparation(input) {
|
|
5489
|
+
return readPreparation(input, this.portalCall());
|
|
5490
|
+
}
|
|
5491
|
+
async answerPreparation(input) {
|
|
5492
|
+
return answerPreparation(input, this.portalCall());
|
|
5493
|
+
}
|
|
5494
|
+
portalCall() {
|
|
5495
|
+
return {
|
|
5496
|
+
token: this.tokenFn,
|
|
5497
|
+
refresh: async () => !this.opts.token && refreshStoredCredential(this.endpoint, { fetch: this.fetchImpl }),
|
|
5498
|
+
fetch: this.fetchImpl
|
|
5499
|
+
};
|
|
5500
|
+
}
|
|
5501
|
+
async getCapabilities(call2 = {}) {
|
|
5502
|
+
const r = await this.transport.request("GET", "/v1/capabilities", void 0, call2);
|
|
5397
5503
|
return r.body;
|
|
5398
5504
|
}
|
|
5399
|
-
async createRepository(options = {},
|
|
5505
|
+
async createRepository(options = {}, call2 = {}) {
|
|
5400
5506
|
const body = {
|
|
5401
5507
|
creationId: options.creationId ?? newCreationId(),
|
|
5402
5508
|
name: options.name ?? "",
|
|
@@ -5404,29 +5510,29 @@ var Publisher = class {
|
|
|
5404
5510
|
objectFormat: "sha1",
|
|
5405
5511
|
grants: options.grants ?? []
|
|
5406
5512
|
};
|
|
5407
|
-
const r = await this.transport.request("POST", "/v1/repos", body,
|
|
5513
|
+
const r = await this.transport.request("POST", "/v1/repos", body, call2);
|
|
5408
5514
|
return r.body.repository;
|
|
5409
5515
|
}
|
|
5410
|
-
async getRepository(repoId,
|
|
5411
|
-
const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0,
|
|
5516
|
+
async getRepository(repoId, call2 = {}) {
|
|
5517
|
+
const r = await this.transport.request("GET", `/v1/repos/${encodeURIComponent(repoId)}`, void 0, call2);
|
|
5412
5518
|
return r.body.repository;
|
|
5413
5519
|
}
|
|
5414
5520
|
/** One page of the repositories the caller created (ADR-0011). An authenticated route, unlike capabilities. */
|
|
5415
|
-
async listRepositories(options = {},
|
|
5521
|
+
async listRepositories(options = {}, call2 = {}) {
|
|
5416
5522
|
const q = new URLSearchParams();
|
|
5417
5523
|
if (options.state) q.set("state", options.state);
|
|
5418
5524
|
if (options.limit !== void 0) q.set("limit", String(options.limit));
|
|
5419
5525
|
if (options.cursor) q.set("cursor", options.cursor);
|
|
5420
5526
|
const qs = q.toString() ? `?${q.toString()}` : "";
|
|
5421
|
-
return (await this.transport.request("GET", `/v1/repos${qs}`, void 0,
|
|
5527
|
+
return (await this.transport.request("GET", `/v1/repos${qs}`, void 0, call2)).body;
|
|
5422
5528
|
}
|
|
5423
|
-
async getPublicationStatus(repoId, publicationId,
|
|
5424
|
-
const q =
|
|
5529
|
+
async getPublicationStatus(repoId, publicationId, call2 = {}) {
|
|
5530
|
+
const q = call2.waitSeconds ? `?wait=${Math.min(call2.waitSeconds, MAX_WAIT_SECONDS)}` : "";
|
|
5425
5531
|
const r = await this.transport.request(
|
|
5426
5532
|
"GET",
|
|
5427
5533
|
`/v1/repos/${encodeURIComponent(repoId)}/publications/${encodeURIComponent(publicationId)}${q}`,
|
|
5428
5534
|
void 0,
|
|
5429
|
-
|
|
5535
|
+
call2
|
|
5430
5536
|
);
|
|
5431
5537
|
return r.body.publication;
|
|
5432
5538
|
}
|
|
@@ -5453,32 +5559,32 @@ var Publisher = class {
|
|
|
5453
5559
|
return { repoId: a.repoId, remote: a.remote };
|
|
5454
5560
|
}
|
|
5455
5561
|
/** Publish existing local commits. Never modifies the worktree; safe to retry and resume. */
|
|
5456
|
-
async publish(options,
|
|
5562
|
+
async publish(options, call2 = {}) {
|
|
5457
5563
|
const { repoId, remote } = await this.resolveRepo(options.repoDir, options.repoId);
|
|
5458
|
-
const outcome = await publish(this.publishContext(), repoId, remote, options,
|
|
5564
|
+
const outcome = await publish(this.publishContext(), repoId, remote, options, call2);
|
|
5459
5565
|
await noteAcceptedPublication(options.repoDir, outcome);
|
|
5460
5566
|
return outcome;
|
|
5461
5567
|
}
|
|
5462
5568
|
// ---- workspaces (docs/workspace-parity.md) ------------------------------------------------
|
|
5463
5569
|
/** Create a NEW editable workspace (clone + mapping) for a published repository. */
|
|
5464
|
-
async checkout(options,
|
|
5465
|
-
return checkoutWorkspace(this, options,
|
|
5570
|
+
async checkout(options, call2 = {}) {
|
|
5571
|
+
return checkoutWorkspace(this, options, call2);
|
|
5466
5572
|
}
|
|
5467
5573
|
/** Make an associated folder a workspace (idempotent); `init` and `sync` both call it. */
|
|
5468
|
-
async adoptWorkspace(directory,
|
|
5469
|
-
return adoptWorkspace(this, directory,
|
|
5574
|
+
async adoptWorkspace(directory, call2 = {}) {
|
|
5575
|
+
return adoptWorkspace(this, directory, call2);
|
|
5470
5576
|
}
|
|
5471
5577
|
/** Offline: the persisted workspace mapping, or null. */
|
|
5472
5578
|
async workspace(directory) {
|
|
5473
5579
|
return getWorkspaceMapping(directory);
|
|
5474
5580
|
}
|
|
5475
5581
|
/** Where the workspace stands relative to the server (decided on real objects). */
|
|
5476
|
-
async workspaceStatus(directory,
|
|
5477
|
-
return inspectWorkspace(this, directory,
|
|
5582
|
+
async workspaceStatus(directory, call2 = {}) {
|
|
5583
|
+
return inspectWorkspace(this, directory, call2);
|
|
5478
5584
|
}
|
|
5479
5585
|
/** Fetch and, with `apply`, fast-forward or merge cleanly; advances the base. */
|
|
5480
|
-
async sync(options,
|
|
5481
|
-
return syncWorkspace(this, options,
|
|
5586
|
+
async sync(options, call2 = {}) {
|
|
5587
|
+
return syncWorkspace(this, options, call2);
|
|
5482
5588
|
}
|
|
5483
5589
|
/**
|
|
5484
5590
|
* A creator's whole round trip in one call, so nobody runs git: record their changes, bring in
|
|
@@ -5490,7 +5596,7 @@ var Publisher = class {
|
|
|
5490
5596
|
* a commit and then an error. `syncWorkspace` itself still never commits or publishes; this is
|
|
5491
5597
|
* the composition, and the only one.
|
|
5492
5598
|
*/
|
|
5493
|
-
async syncChanges(options,
|
|
5599
|
+
async syncChanges(options, call2 = {}) {
|
|
5494
5600
|
const dir = options.directory;
|
|
5495
5601
|
await this.resolveRepo(dir);
|
|
5496
5602
|
const branch = (await currentBranch(dir))?.replace(/^refs\/heads\//, "") ?? null;
|
|
@@ -5499,7 +5605,7 @@ var Publisher = class {
|
|
|
5499
5605
|
if (mapped && mapped.branch !== branch)
|
|
5500
5606
|
throw new PublisherError("LOCAL_STATE", `branch ${branch} is checked out, but this folder publishes ${mapped.branch}; check it out, then run sync again`);
|
|
5501
5607
|
const recorded = await recordChanges({ dir, ...options.message ? { message: options.message } : {} });
|
|
5502
|
-
const reconciled = await syncWorkspace(this, { directory: dir, apply: true },
|
|
5608
|
+
const reconciled = await syncWorkspace(this, { directory: dir, apply: true }, call2);
|
|
5503
5609
|
const base = { recorded: { commit: recorded.commit, committed: recorded.committed, excluded: recorded.excluded }, reconciled };
|
|
5504
5610
|
if (reconciled.after === "conflict") {
|
|
5505
5611
|
const remote = reconciled.before.remote?.oid ?? null;
|
|
@@ -5508,25 +5614,25 @@ var Publisher = class {
|
|
|
5508
5614
|
}
|
|
5509
5615
|
const tip = await revParse(dir, `refs/heads/${branch}`);
|
|
5510
5616
|
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 },
|
|
5617
|
+
const published = await this.publish({ repoDir: dir }, call2);
|
|
5512
5618
|
return { ...base, state: published.state === "accepted" ? "published" : published.state === "rejected" ? "rejected" : "unknown", published, marked: false };
|
|
5513
5619
|
}
|
|
5514
5620
|
/** Resolve interrupted publications recorded locally without creating duplicates. */
|
|
5515
|
-
async resumePublications(repoDir,
|
|
5621
|
+
async resumePublications(repoDir, call2 = {}) {
|
|
5516
5622
|
const { remote } = await this.resolveRepo(repoDir);
|
|
5517
5623
|
const out = [];
|
|
5518
5624
|
const store2 = await openPending(repoDir);
|
|
5519
5625
|
for (const p of await store2.list()) {
|
|
5520
5626
|
if (p.endpoint !== this.endpoint) continue;
|
|
5521
5627
|
if (!p.finalized) {
|
|
5522
|
-
out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId },
|
|
5628
|
+
out.push(await publish(this.publishContext(), p.repoId, remote, { repoDir, publicationId: p.publicationId }, call2).catch((e) => rethrowUnlessResolvable(e, p)));
|
|
5523
5629
|
continue;
|
|
5524
5630
|
}
|
|
5525
|
-
const deadline = Date.now() + (
|
|
5526
|
-
let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...
|
|
5631
|
+
const deadline = Date.now() + (call2.waitTimeoutMs ?? 5 * 6e4);
|
|
5632
|
+
let s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5527
5633
|
while (s.state === "finalizing" && Date.now() < deadline) {
|
|
5528
|
-
|
|
5529
|
-
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...
|
|
5634
|
+
call2.onProgress?.({ phase: "validate", message: `waiting for ${p.publicationId}` });
|
|
5635
|
+
s = await resolvePending(this.publishContext(), repoDir, remote, p, { ...call2, waitSeconds: MAX_WAIT_SECONDS }, store2);
|
|
5530
5636
|
}
|
|
5531
5637
|
out.push(toOutcome(s, true, p.refUpdates));
|
|
5532
5638
|
}
|
|
@@ -5541,17 +5647,17 @@ var Publisher = class {
|
|
|
5541
5647
|
* branches. Every read is a presigned download of the repository's bundle and packs (ADR-0013);
|
|
5542
5648
|
* artifacts already installed are not downloaded again.
|
|
5543
5649
|
*/
|
|
5544
|
-
async fetch(options,
|
|
5650
|
+
async fetch(options, call2 = {}) {
|
|
5545
5651
|
const { repoId } = await this.resolveRepo(options.repoDir, options.repoId);
|
|
5546
|
-
return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options,
|
|
5652
|
+
return fetchRepository({ transport: this.transport, fetchImpl: this.fetchImpl }, repoId, options, call2);
|
|
5547
5653
|
}
|
|
5548
5654
|
publishContext() {
|
|
5549
5655
|
return {
|
|
5550
5656
|
transport: this.transport,
|
|
5551
5657
|
fetchImpl: this.fetchImpl,
|
|
5552
5658
|
clientInfo: this.clientInfo,
|
|
5553
|
-
getRepository: (repoId,
|
|
5554
|
-
getStatus: (repoId, publicationId,
|
|
5659
|
+
getRepository: (repoId, call2) => this.getRepository(repoId, call2),
|
|
5660
|
+
getStatus: (repoId, publicationId, call2) => this.getPublicationStatus(repoId, publicationId, call2)
|
|
5555
5661
|
};
|
|
5556
5662
|
}
|
|
5557
5663
|
};
|
|
@@ -5717,7 +5823,7 @@ function resolveChoice(answer, names) {
|
|
|
5717
5823
|
}
|
|
5718
5824
|
return names.find((n) => n.toLowerCase() === a.toLowerCase()) ?? null;
|
|
5719
5825
|
}
|
|
5720
|
-
async function
|
|
5826
|
+
async function ask3(question) {
|
|
5721
5827
|
const rl = (0, import_promises10.createInterface)({ input: process.stdin, output: process.stderr });
|
|
5722
5828
|
try {
|
|
5723
5829
|
return await rl.question(question);
|
|
@@ -5735,7 +5841,7 @@ ${purpose}
|
|
|
5735
5841
|
names.forEach((n, i) => process.stderr.write(` ${i + 1}) ${n}
|
|
5736
5842
|
`));
|
|
5737
5843
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5738
|
-
const answer = await
|
|
5844
|
+
const answer = await ask3(`
|
|
5739
5845
|
Which one? [1-${names.length}, or a name] `);
|
|
5740
5846
|
if (answer === null) return null;
|
|
5741
5847
|
const chosen = resolveChoice(answer, names);
|
|
@@ -5751,7 +5857,7 @@ Name this connection, so you never have to type ${endpoint} again.
|
|
|
5751
5857
|
(for example: prod, dev, staging)
|
|
5752
5858
|
`);
|
|
5753
5859
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
5754
|
-
const answer = await
|
|
5860
|
+
const answer = await ask3(suggested ? `
|
|
5755
5861
|
Profile name [${suggested}]: ` : `
|
|
5756
5862
|
Profile name: `);
|
|
5757
5863
|
if (answer === null) return null;
|
|
@@ -5874,8 +5980,9 @@ async function followPublication(p, endpoint, o, opts) {
|
|
|
5874
5980
|
{ portal, repoId: o.repoId, publicationId: o.publicationId, ref, commit },
|
|
5875
5981
|
{ onStage: opts.onStage, ...opts.timeoutMs === void 0 ? {} : { timeoutMs: opts.timeoutMs } }
|
|
5876
5982
|
);
|
|
5877
|
-
const
|
|
5878
|
-
|
|
5983
|
+
const answerable = confirmed.terminal.includes(PREPARATION);
|
|
5984
|
+
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;
|
|
5985
|
+
return { ...outcome, portal, hint: progressHint(outcome, portal, answerable), ...next === void 0 ? {} : { next } };
|
|
5879
5986
|
}
|
|
5880
5987
|
function followTimeoutMs(flag) {
|
|
5881
5988
|
if (flag === void 0) return FOLLOW_TIMEOUT_MS;
|
|
@@ -5888,8 +5995,10 @@ function stageLine(e, json) {
|
|
|
5888
5995
|
function inPortal(portal, lead, verb) {
|
|
5889
5996
|
return `${lead} ${verb} in the portal${portal === null ? "" : `: ${portal}`}.`;
|
|
5890
5997
|
}
|
|
5891
|
-
function progressHint(r, portal) {
|
|
5998
|
+
function progressHint(r, portal, answerable = false) {
|
|
5892
5999
|
switch (r.kind) {
|
|
6000
|
+
case "waiting":
|
|
6001
|
+
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
6002
|
case "settled":
|
|
5894
6003
|
switch (r.ending) {
|
|
5895
6004
|
case "live":
|
|
@@ -5915,6 +6024,78 @@ function progressHint(r, portal) {
|
|
|
5915
6024
|
}
|
|
5916
6025
|
}
|
|
5917
6026
|
|
|
6027
|
+
// src/question.ts
|
|
6028
|
+
async function questionAt(p, endpoint, repoId, opts = {}) {
|
|
6029
|
+
const at = await portalAt(endpoint, opts);
|
|
6030
|
+
if ("reason" in at) return at;
|
|
6031
|
+
return shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6032
|
+
}
|
|
6033
|
+
async function answerAt(p, endpoint, repoId, specs, opts = {}) {
|
|
6034
|
+
const answers = parseSpecs(specs);
|
|
6035
|
+
const at = await portalAt(endpoint, opts);
|
|
6036
|
+
if ("reason" in at) return at;
|
|
6037
|
+
const read = shown(at.portal, await p.readPreparation({ portal: at.portal, repoId }));
|
|
6038
|
+
if (read.kind !== "view") return read;
|
|
6039
|
+
if (read.questions.length === 0) throw new PublisherError("LOCAL_STATE", `nothing is waiting for an answer (the preparation is ${read.preparation.status})`);
|
|
6040
|
+
for (const [id, value] of Object.entries(answers)) {
|
|
6041
|
+
const question = read.questions.find((q) => q.id === id);
|
|
6042
|
+
if (question === void 0) {
|
|
6043
|
+
throw new PublisherError("INVALID_REQUEST", `no pending question "${id}"; pending: ${read.questions.map((q) => q.id).join(", ")}`, {
|
|
6044
|
+
details: { questions: read.questions }
|
|
6045
|
+
});
|
|
6046
|
+
}
|
|
6047
|
+
const values = question.options.map((o) => o.value);
|
|
6048
|
+
if (!values.includes(value)) {
|
|
6049
|
+
throw new PublisherError("INVALID_REQUEST", `"${value}" is not one of ${values.join(", ")} for "${id}"`, { details: { questions: read.questions } });
|
|
6050
|
+
}
|
|
6051
|
+
}
|
|
6052
|
+
return shown(at.portal, await p.answerPreparation({ portal: at.portal, repoId, id: read.preparation.id, revision: read.preparation.revision, answers }));
|
|
6053
|
+
}
|
|
6054
|
+
function parseSpecs(specs) {
|
|
6055
|
+
if (specs.length === 0) throw new PublisherError("INVALID_REQUEST", "answer takes one or more question=value");
|
|
6056
|
+
const answers = {};
|
|
6057
|
+
for (const spec of specs) {
|
|
6058
|
+
const at = spec.indexOf("=");
|
|
6059
|
+
if (at <= 0 || at === spec.length - 1) throw new PublisherError("INVALID_REQUEST", `"${spec}" is not question=value`);
|
|
6060
|
+
answers[spec.slice(0, at)] = spec.slice(at + 1);
|
|
6061
|
+
}
|
|
6062
|
+
return answers;
|
|
6063
|
+
}
|
|
6064
|
+
async function portalAt(endpoint, opts) {
|
|
6065
|
+
const derived = portalForEndpoint(endpoint);
|
|
6066
|
+
const confirmed = derived === null ? null : await confirmPortal(derived, endpoint, opts.fetch);
|
|
6067
|
+
if (derived === null || confirmed === null) return { kind: "unavailable", reason: "no-portal", portal: null, hint: "Answer it in the portal." };
|
|
6068
|
+
if (!confirmed.terminal.includes(PREPARATION)) {
|
|
6069
|
+
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}.` };
|
|
6070
|
+
}
|
|
6071
|
+
return { portal: derived };
|
|
6072
|
+
}
|
|
6073
|
+
function shown(portal, r) {
|
|
6074
|
+
switch (r.kind) {
|
|
6075
|
+
case "view": {
|
|
6076
|
+
const { questions, ...preparation } = r.view;
|
|
6077
|
+
return { kind: "view", portal, preparation, questions, hint: hintFor(r.view) };
|
|
6078
|
+
}
|
|
6079
|
+
case "no-application":
|
|
6080
|
+
throw new PublisherError("NOT_FOUND", "this repository has no application in your organisation yet; publish it first");
|
|
6081
|
+
case "moved-on":
|
|
6082
|
+
throw new PublisherError("UNAVAILABLE", "the preparation moved on before the answer arrived; run publisher question again", { retryable: true });
|
|
6083
|
+
case "unauthorized":
|
|
6084
|
+
throw new PublisherError("UNAUTHORIZED", "the portal did not accept this sign-in; run publisher login");
|
|
6085
|
+
case "forbidden":
|
|
6086
|
+
throw new PublisherError("FORBIDDEN", "the portal will not let this sign-in answer for this organisation (an owner or admin can)");
|
|
6087
|
+
case "unavailable":
|
|
6088
|
+
throw new PublisherError("UNAVAILABLE", `the portal could not be asked: ${r.detail}`, { retryable: true });
|
|
6089
|
+
case "malformed":
|
|
6090
|
+
throw new PublisherError("UNAVAILABLE", `the portal's answer could not be read: ${r.detail}`);
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
function hintFor(view) {
|
|
6094
|
+
if (view.questions.length === 0) return "Nothing is waiting for an answer.";
|
|
6095
|
+
const how = view.questions.map((q) => `${q.id}=<${q.options.map((o) => o.value).join("|")}>`).join(" ");
|
|
6096
|
+
return `It is waiting for your answer. Run: publisher answer ${how}`;
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5918
6099
|
// src/main.ts
|
|
5919
6100
|
var USAGE = `publisher \u2014 publish an app's Git history to the serverless Git backend
|
|
5920
6101
|
|
|
@@ -5951,6 +6132,10 @@ Usage:
|
|
|
5951
6132
|
result's next.command picks it up
|
|
5952
6133
|
publisher follow [dir] --publication <id> follow an accepted publication's run where a sync left it
|
|
5953
6134
|
[--timeout <seconds>]
|
|
6135
|
+
publisher question [dir] [--repo <id>] the question the app's preparation waits on, if any,
|
|
6136
|
+
with each option's value (answering needs owner or admin)
|
|
6137
|
+
publisher answer <question>=<value>... [--repo <id>] answer it from this folder; a value that is not one of
|
|
6138
|
+
the question's options is refused before anything is sent
|
|
5954
6139
|
publisher merge-check [dir] --ours <ref> --theirs <ref> [--apply]
|
|
5955
6140
|
publisher info [dir] repository state as seen by the server
|
|
5956
6141
|
publisher doctor environment check (node, git, credentials, association)
|
|
@@ -6322,6 +6507,27 @@ Waiting (it expires in ${Math.round(expiresInSeconds / 60)} minutes)...
|
|
|
6322
6507
|
out({ publicationId, progress: await followPublication(p, endpoint, publication, { onStage: emitStage, log, timeoutMs }) });
|
|
6323
6508
|
return 0;
|
|
6324
6509
|
}
|
|
6510
|
+
/**
|
|
6511
|
+
* The business question the app's current preparation waits on, from this folder's repository.
|
|
6512
|
+
* Exit 0 whether or not one is pending: `questions` says which. An outage is exit 3, never "none".
|
|
6513
|
+
*/
|
|
6514
|
+
case "question": {
|
|
6515
|
+
const dir = dirArg(1);
|
|
6516
|
+
const { p, endpoint } = await client(dir);
|
|
6517
|
+
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6518
|
+
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
6519
|
+
out({ repoId, ...await questionAt(p, endpoint, repoId) });
|
|
6520
|
+
return 0;
|
|
6521
|
+
}
|
|
6522
|
+
/** Answer it, as `question=value` pairs, re-reading the preparation first. */
|
|
6523
|
+
case "answer": {
|
|
6524
|
+
const dir = (0, import_node_path8.resolve)(".");
|
|
6525
|
+
const { p, endpoint } = await client(dir);
|
|
6526
|
+
const repoId = str2(flags["repo"]) ?? (await readAssociation(dir))?.repoId;
|
|
6527
|
+
if (!repoId) throw new PublisherError("LOCAL_STATE", "no repository association; pass --repo");
|
|
6528
|
+
out({ repoId, ...await answerAt(p, endpoint, repoId, cmd.slice(1)) });
|
|
6529
|
+
return 0;
|
|
6530
|
+
}
|
|
6325
6531
|
case "skills": {
|
|
6326
6532
|
if (cmd[1] !== "install") throw new PublisherError("INVALID_REQUEST", `unknown skills command: ${cmd[1] ?? "(none)"}; expected install`);
|
|
6327
6533
|
const r = await installSkills({ log });
|