@allocator-one/harness-cli 1.15.1 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -38
- package/dist/{chunk-L2OWOZ4N.js → chunk-GVTZKAXC.js} +194 -46
- package/dist/cli.js +277 -52
- package/dist/{server-KVRQBVXI.js → server-F6YRKNC2.js} +27 -11
- package/package.json +1 -1
- package/spec/openapi.json +1392 -25
|
@@ -331,6 +331,7 @@ function issueDetail(issue, images = []) {
|
|
|
331
331
|
["Priority", issue["priority_label"]],
|
|
332
332
|
["Assignee", path(issue, "assignee", "name")],
|
|
333
333
|
["Team", path(issue, "team", "key")],
|
|
334
|
+
["Plan", issuePlanLine(issue["plan"])],
|
|
334
335
|
["Labels", labelsLine(issue["labels"])],
|
|
335
336
|
["Blocked", blockedLine(issue)],
|
|
336
337
|
["Waiting", waitingLine(issue)],
|
|
@@ -735,6 +736,34 @@ function roleName(role) {
|
|
|
735
736
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
736
737
|
}
|
|
737
738
|
var LIVE_PLAN_STATUSES = ["released", "running", "needs_attention"];
|
|
739
|
+
function plansTable(plans) {
|
|
740
|
+
const rows = plans.map((plan) => [
|
|
741
|
+
str(plan["root"]) || "-",
|
|
742
|
+
str(plan["status"]) || "-",
|
|
743
|
+
plan["held"] === true ? "yes" : plan["held"] === false ? "no" : "-",
|
|
744
|
+
typeof plan["revision"] === "number" ? String(plan["revision"]) : "-",
|
|
745
|
+
truncate(plan["title"])
|
|
746
|
+
]);
|
|
747
|
+
return table(["ROOT", "STATUS", "HELD", "REVISION", "TITLE"], rows);
|
|
748
|
+
}
|
|
749
|
+
function issuePlanLine(plan) {
|
|
750
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return null;
|
|
751
|
+
const record = plan;
|
|
752
|
+
const root = clean(record["root"]);
|
|
753
|
+
if (root === "") return null;
|
|
754
|
+
const status = record["held"] === true ? "held" : clean(record["status"]);
|
|
755
|
+
const revision = record["revision"];
|
|
756
|
+
const parts = [root];
|
|
757
|
+
if (status !== "") parts.push(status);
|
|
758
|
+
if (typeof revision === "number") {
|
|
759
|
+
parts.push(`revision ${revision}`);
|
|
760
|
+
if (typeof record["in_revision"] === "boolean") parts.push(record["in_revision"] ? "in revision" : "not in revision");
|
|
761
|
+
} else {
|
|
762
|
+
parts.push("never released");
|
|
763
|
+
}
|
|
764
|
+
if (record["executor"] === "operator") parts.push("Operator step");
|
|
765
|
+
return parts.join(" \xB7 ");
|
|
766
|
+
}
|
|
738
767
|
function planDetail(plan) {
|
|
739
768
|
const steps = records(plan["steps"]);
|
|
740
769
|
const fields = [
|
|
@@ -863,7 +892,7 @@ function holderText(holder) {
|
|
|
863
892
|
function planHeldSection(heldSteps) {
|
|
864
893
|
if (!Array.isArray(heldSteps) || heldSteps.length === 0) return "";
|
|
865
894
|
return `
|
|
866
|
-
|
|
895
|
+
Changed since the release: ${heldSteps.map((step) => clean(step)).join(", ")} (only Operator steps are held until the next release takes them in)
|
|
867
896
|
`;
|
|
868
897
|
}
|
|
869
898
|
function indentLines(text) {
|
|
@@ -883,11 +912,32 @@ function changesetDetails(details) {
|
|
|
883
912
|
function violationDetails(violations) {
|
|
884
913
|
return records(violations).map((violation) => {
|
|
885
914
|
const code = clean(violation["code"]);
|
|
886
|
-
const message = clean(violation["message"]) || code || "invalid";
|
|
915
|
+
const message = capMessage(violation) ?? (clean(violation["message"]) || code || "invalid");
|
|
887
916
|
return `
|
|
888
917
|
${violationSubject(violation)}: ${message}${code === "" ? "" : ` [${code}]`}`;
|
|
889
918
|
}).join("");
|
|
890
919
|
}
|
|
920
|
+
var MAX_PLAN_STEPS_CEILING = 2e3;
|
|
921
|
+
function capMessage(violation) {
|
|
922
|
+
if (violation["code"] !== "too_many_steps") return null;
|
|
923
|
+
const cap = violation["cap"];
|
|
924
|
+
if (typeof cap !== "object" || cap === null || Array.isArray(cap)) return null;
|
|
925
|
+
const { kind, max, count, ceiling: served } = cap;
|
|
926
|
+
if (!Number.isInteger(max) || !Number.isInteger(count)) return null;
|
|
927
|
+
if (kind === "operator_steps") {
|
|
928
|
+
return `the plan has ${count} Operator steps, above the fixed limit of ${max}: Operator's delegated grant caps a plan's agent steps, and no setting raises it \u2014 hand some to people (executor none) or split the plan`;
|
|
929
|
+
}
|
|
930
|
+
if (kind === "plan_steps") {
|
|
931
|
+
const ceiling = Number.isInteger(served) ? served : MAX_PLAN_STEPS_CEILING;
|
|
932
|
+
const over = `the plan has ${count} steps, above this organization's max plan steps (${max})`;
|
|
933
|
+
if (count > ceiling && max < ceiling) {
|
|
934
|
+
return `${over}, and above the ceiling of ${ceiling} that no setting raises past \u2014 split the plan`;
|
|
935
|
+
}
|
|
936
|
+
if (max >= ceiling) return `${over}, which is already at its ceiling of ${ceiling} \u2014 split the plan`;
|
|
937
|
+
return `${over}: a Harness admin can raise "Max plan steps" in Harness settings \u2192 Plans (max_plan_steps, up to ${ceiling})`;
|
|
938
|
+
}
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
891
941
|
function violationSubject(violation) {
|
|
892
942
|
const key = typeof violation["key"] === "string" ? clean(violation["key"]) : null;
|
|
893
943
|
const index = typeof violation["index"] === "number" ? `steps[${violation["index"]}]` : null;
|
|
@@ -1185,6 +1235,9 @@ var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}
|
|
|
1185
1235
|
function isUuid(value) {
|
|
1186
1236
|
return UUID_FORMAT.test(value);
|
|
1187
1237
|
}
|
|
1238
|
+
function isIdentifier(value) {
|
|
1239
|
+
return IDENTIFIER_FORMAT.test(value);
|
|
1240
|
+
}
|
|
1188
1241
|
function requireIdentifiers(identifiers) {
|
|
1189
1242
|
const bad = identifiers.filter((identifier) => !IDENTIFIER_FORMAT.test(identifier));
|
|
1190
1243
|
if (bad.length > 0) {
|
|
@@ -1514,18 +1567,42 @@ ${opts.appendNotes}`;
|
|
|
1514
1567
|
return { json: issue, human };
|
|
1515
1568
|
}
|
|
1516
1569
|
var CLAIM_REFUSAL_HINTS = {
|
|
1517
|
-
//
|
|
1518
|
-
// the step is
|
|
1519
|
-
|
|
1570
|
+
// A person never claims an Operator step, so releasing the plan is not the
|
|
1571
|
+
// fix: handing the step to a person is, and a handed-over Operator step
|
|
1572
|
+
// stays frozen until the next release.
|
|
1573
|
+
agent_step: (_identifier, plan) => {
|
|
1574
|
+
const runs = plan.kind === "sole" ? `plan ${plan.root}` : plan.kind === "nearest" ? `its plans (several govern it; the nearest is ${plan.root})` : "its plan";
|
|
1575
|
+
const target = plan.kind === "sole" ? plan.root : "<plan>";
|
|
1576
|
+
return `a person can't claim an Operator step: Operator runs it for ${runs}. To do it yourself, set its executor to none (${invokedName()} plan edit ${target}) and release the plan again`;
|
|
1577
|
+
},
|
|
1578
|
+
plan_not_released: (_identifier, plan) => planReleaseHint(plan),
|
|
1579
|
+
not_in_plan_revision: (_identifier, plan) => planReleaseHint(plan),
|
|
1580
|
+
edited_since_release: (_identifier, plan) => planReleaseHint(plan),
|
|
1520
1581
|
waiting_on_condition: (identifier) => `${invokedName()} show ${identifier} lists what it waits on; tick a gate with ${invokedName()} gate satisfy ${identifier} <gate>`
|
|
1521
1582
|
};
|
|
1583
|
+
function planReleaseHint(plan) {
|
|
1584
|
+
if (plan.kind === "sole") return plan.release ? `run ${invokedName()} plan release ${plan.root}` : void 0;
|
|
1585
|
+
if (plan.kind === "nearest") {
|
|
1586
|
+
return `several plans govern it; the nearest is ${plan.root} (${invokedName()} plan show ${plan.root})`;
|
|
1587
|
+
}
|
|
1588
|
+
return void 0;
|
|
1589
|
+
}
|
|
1522
1590
|
function explainClaimRefusal(error, identifier) {
|
|
1523
1591
|
if (!(error instanceof HttpError) || error.status !== 409) return error;
|
|
1524
1592
|
const reason = error.body["reason"];
|
|
1525
|
-
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier) : void 0;
|
|
1593
|
+
const hint = typeof reason === "string" && Object.hasOwn(CLAIM_REFUSAL_HINTS, reason) ? CLAIM_REFUSAL_HINTS[reason](identifier, refusingPlan(error.body)) : void 0;
|
|
1526
1594
|
if (hint === void 0) return error;
|
|
1527
1595
|
return new HttpError(error.status, `${error.message} \u2014 ${hint}`, error.body, error.retryAfter);
|
|
1528
1596
|
}
|
|
1597
|
+
function refusingPlan(body) {
|
|
1598
|
+
const plan = body["plan"];
|
|
1599
|
+
if (typeof plan !== "object" || plan === null || Array.isArray(plan)) return { kind: "none" };
|
|
1600
|
+
const root = plan["root"];
|
|
1601
|
+
if (typeof root !== "string" || !isIdentifier(root)) return { kind: "none" };
|
|
1602
|
+
const message = typeof body["message"] === "string" ? body["message"] : "";
|
|
1603
|
+
if (!new RegExp(`\\bplan ${root}\\b`).test(message)) return { kind: "nearest", root };
|
|
1604
|
+
return { kind: "sole", root, release: new RegExp(`\\brelease plan ${root}\\b`).test(message) };
|
|
1605
|
+
}
|
|
1529
1606
|
function requireCloseArgs(identifiers, opts) {
|
|
1530
1607
|
if (identifiers.length === 0) {
|
|
1531
1608
|
throw new UsageError(
|
|
@@ -4273,6 +4350,9 @@ import { parseDocument } from "yaml";
|
|
|
4273
4350
|
var PLAN_EXECUTORS = ["none", "operator"];
|
|
4274
4351
|
var CONDITION_KINDS = ["not_before", "gate", "event"];
|
|
4275
4352
|
var GATE_OWNER_ROLES = ["gp", "compliance_officer", "valuer", "accountant", "auditor", "counsel"];
|
|
4353
|
+
var PLAN_STATUSES = ["held", "released", "running", "completed", "needs_attention"];
|
|
4354
|
+
var PLAN_LIST_MAX_LIMIT = 100;
|
|
4355
|
+
var PLAN_LIST_DEFAULT_LIMIT = 50;
|
|
4276
4356
|
function parsePlanDocument(text, source) {
|
|
4277
4357
|
const parsed = extname2(source).toLowerCase() === ".json" ? parseJson(text, source) : parseYamlDocument(text, source);
|
|
4278
4358
|
if (!isRecord5(parsed)) {
|
|
@@ -4335,13 +4415,44 @@ Nothing runs until it is released: ${invokedName()} plan release ${clean(id)}
|
|
|
4335
4415
|
`
|
|
4336
4416
|
};
|
|
4337
4417
|
}
|
|
4418
|
+
function requirePlanListArgs(opts) {
|
|
4419
|
+
if (opts.status !== void 0 && !PLAN_STATUSES.includes(opts.status)) {
|
|
4420
|
+
throw new UsageError(`status must be one of: ${PLAN_STATUSES.join(", ")}`);
|
|
4421
|
+
}
|
|
4422
|
+
if (opts.limit !== void 0 && (!Number.isInteger(opts.limit) || opts.limit < 1 || opts.limit > PLAN_LIST_MAX_LIMIT)) {
|
|
4423
|
+
throw new UsageError(`--limit takes 1 to ${PLAN_LIST_MAX_LIMIT}: the server returns at most ${PLAN_LIST_MAX_LIMIT} plans`);
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4426
|
+
async function planList(ctx, opts) {
|
|
4427
|
+
requirePlanListArgs(opts);
|
|
4428
|
+
const team = requiredTeam(ctx, opts.team);
|
|
4429
|
+
let body;
|
|
4430
|
+
try {
|
|
4431
|
+
body = await ctx.client.get(`/api/v1/teams/${team}/plans`, { status: opts.status, limit: opts.limit });
|
|
4432
|
+
} catch (error) {
|
|
4433
|
+
throw explainMissing(error, `no team ${team} here, or this Harness server cannot list plans yet`);
|
|
4434
|
+
}
|
|
4435
|
+
if (!Array.isArray(body.data)) throw new CommandError("The plan list response carried no list.");
|
|
4436
|
+
const list3 = body.data.filter(isRecord5);
|
|
4437
|
+
const limit = opts.limit ?? PLAN_LIST_DEFAULT_LIMIT;
|
|
4438
|
+
let notice;
|
|
4439
|
+
if (list3.length >= limit) {
|
|
4440
|
+
notice = limit < PLAN_LIST_MAX_LIMIT ? `harness: showing the newest ${list3.length} plans \u2014 pass --limit (up to ${PLAN_LIST_MAX_LIMIT}) for older ones.
|
|
4441
|
+
` : `harness: showing the newest ${list3.length} plans, the most one list returns \u2014 narrow it with --status.
|
|
4442
|
+
`;
|
|
4443
|
+
}
|
|
4444
|
+
const what = opts.status === void 0 ? "plans" : `${opts.status} plans`;
|
|
4445
|
+
const human = list3.length === 0 ? `No ${what} in team ${team}.
|
|
4446
|
+
` : plansTable(list3);
|
|
4447
|
+
return notice === void 0 ? { json: body.data, human } : { json: body.data, human, notice };
|
|
4448
|
+
}
|
|
4338
4449
|
async function planShow(ctx, id) {
|
|
4339
|
-
|
|
4450
|
+
requirePlanRef(id);
|
|
4340
4451
|
const plan = await planRequest(id, () => ctx.client.get(`/api/v1/plans/${id}`));
|
|
4341
4452
|
return { json: plan, human: planDetail(plan) };
|
|
4342
4453
|
}
|
|
4343
4454
|
async function planEdit(ctx, id, edits) {
|
|
4344
|
-
|
|
4455
|
+
requirePlanRef(id);
|
|
4345
4456
|
requireDocument(edits, "a plan edit is one object: steps to change and budgets");
|
|
4346
4457
|
const plan = await planRequest(id, () => ctx.client.patch(`/api/v1/plans/${id}`, edits));
|
|
4347
4458
|
const held = Array.isArray(plan["held_steps"]) && plan["held_steps"].length > 0;
|
|
@@ -4350,12 +4461,12 @@ async function planEdit(ctx, id, edits) {
|
|
|
4350
4461
|
human: `Updated plan ${clean(id)}
|
|
4351
4462
|
|
|
4352
4463
|
` + planDetail(plan) + (held ? `
|
|
4353
|
-
|
|
4464
|
+
Only Operator steps are held until the next release; release the plan again to take in the changes: ${invokedName()} plan release ${clean(id)}
|
|
4354
4465
|
` : "")
|
|
4355
4466
|
};
|
|
4356
4467
|
}
|
|
4357
4468
|
async function planRelease(ctx, id, opts) {
|
|
4358
|
-
|
|
4469
|
+
requirePlanRef(id);
|
|
4359
4470
|
const body = {};
|
|
4360
4471
|
if (opts.collaborators !== void 0 && opts.collaborators.length > 0) body["collaborators"] = opts.collaborators;
|
|
4361
4472
|
if (opts.retryBudget !== void 0) body["retry_budget"] = opts.retryBudget;
|
|
@@ -4370,7 +4481,7 @@ async function planRelease(ctx, id, opts) {
|
|
|
4370
4481
|
};
|
|
4371
4482
|
}
|
|
4372
4483
|
async function planHold(ctx, id) {
|
|
4373
|
-
|
|
4484
|
+
requirePlanRef(id);
|
|
4374
4485
|
const plan = await planRequest(id, () => ctx.client.post(`/api/v1/plans/${id}/hold`));
|
|
4375
4486
|
return {
|
|
4376
4487
|
json: plan,
|
|
@@ -4383,25 +4494,41 @@ async function gateSet(ctx, issue, gate, satisfied) {
|
|
|
4383
4494
|
const wanted = gate.trim();
|
|
4384
4495
|
if (wanted === "") throw new UsageError("name the gate by its key, its label or its condition id");
|
|
4385
4496
|
const expert = isExpertToken(ctx.config.token);
|
|
4386
|
-
|
|
4387
|
-
|
|
4497
|
+
let list3 = null;
|
|
4498
|
+
if (!isUuid(wanted)) {
|
|
4499
|
+
try {
|
|
4500
|
+
list3 = await readConditions(ctx, issue, expert);
|
|
4501
|
+
} catch (error) {
|
|
4502
|
+
if (error instanceof HttpError && error.status === 403) {
|
|
4503
|
+
throw new HttpError(
|
|
4504
|
+
error.status,
|
|
4505
|
+
`${error.message} \u2014 find the gate by its condition id; only Harness staff can list an issue's conditions`,
|
|
4506
|
+
error.body,
|
|
4507
|
+
error.retryAfter
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
throw error;
|
|
4511
|
+
}
|
|
4512
|
+
}
|
|
4513
|
+
const target = list3 === null ? { id: wanted.toLowerCase(), label: wanted.toLowerCase() } : findGate(list3, issue, wanted, expert);
|
|
4388
4514
|
const id = str7(target["id"]);
|
|
4389
4515
|
let body;
|
|
4390
4516
|
try {
|
|
4391
4517
|
body = await ctx.client.put(`/api/v1/issues/${issue}/conditions/${id}/satisfaction`, { satisfied });
|
|
4392
4518
|
} catch (error) {
|
|
4393
|
-
throw await explainNotGateOwner(ctx, error, target, issue, satisfied);
|
|
4519
|
+
throw await explainNotGateOwner(ctx, error, target, issue, satisfied, list3 !== null);
|
|
4394
4520
|
}
|
|
4395
|
-
const others = list3
|
|
4521
|
+
const others = list3?.filter((condition) => condition["id"] !== id && condition["satisfied"] !== true).length ?? 0;
|
|
4396
4522
|
let tail = "";
|
|
4397
|
-
if (satisfied && expert) {
|
|
4523
|
+
if (satisfied && list3 !== null && expert) {
|
|
4398
4524
|
tail = others > 0 ? ` \u2014 ${others} other gate${others === 1 ? "" : "s"} of yours on it ${others === 1 ? "is" : "are"} open` : "";
|
|
4399
|
-
} else if (satisfied) {
|
|
4525
|
+
} else if (satisfied && list3 !== null) {
|
|
4400
4526
|
tail = others > 0 ? ` \u2014 it still waits on ${others} other condition${others === 1 ? "" : "s"}` : " \u2014 no other wait condition holds it";
|
|
4401
4527
|
}
|
|
4528
|
+
const label = list3 === null && isRecord5(body.data) ? body.data["label"] ?? id : target["label"];
|
|
4402
4529
|
return {
|
|
4403
4530
|
json: body.data,
|
|
4404
|
-
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(
|
|
4531
|
+
human: `${satisfied ? "Satisfied" : "Un-satisfied"} gate "${clean(label)}" on ${issue}${tail}
|
|
4405
4532
|
`
|
|
4406
4533
|
};
|
|
4407
4534
|
}
|
|
@@ -4435,8 +4562,10 @@ async function readConditions(ctx, issue, expert) {
|
|
|
4435
4562
|
if (!Array.isArray(conditions)) throw new CommandError("The conditions response carried no list.");
|
|
4436
4563
|
return conditions.filter(isRecord5);
|
|
4437
4564
|
}
|
|
4438
|
-
function
|
|
4439
|
-
if (!isUuid(id)
|
|
4565
|
+
function requirePlanRef(id) {
|
|
4566
|
+
if (!isUuid(id) && !isIdentifier(id)) {
|
|
4567
|
+
throw new UsageError(`not a plan: ${id} (expected the plan's id or its root issue's identifier, e.g. FR-1)`);
|
|
4568
|
+
}
|
|
4440
4569
|
}
|
|
4441
4570
|
function requireDocument(value, what) {
|
|
4442
4571
|
if (!isRecord5(value)) throw new UsageError(what);
|
|
@@ -4445,18 +4574,21 @@ async function planRequest(id, send) {
|
|
|
4445
4574
|
try {
|
|
4446
4575
|
return (await send()).data;
|
|
4447
4576
|
} catch (error) {
|
|
4448
|
-
throw explainMissing(
|
|
4577
|
+
throw explainMissing(
|
|
4578
|
+
error,
|
|
4579
|
+
isUuid(id) ? `no plan ${id} in this organization, or this Harness server has no plan API yet` : `${id} roots no plan in this organization (${invokedName()} show ${id} names the plan an issue belongs to, by its root), or this Harness server opens plans by their id only`
|
|
4580
|
+
);
|
|
4449
4581
|
}
|
|
4450
4582
|
}
|
|
4451
4583
|
function explainMissing(error, why) {
|
|
4452
4584
|
if (!(error instanceof HttpError) || error.status !== 404) return error;
|
|
4453
4585
|
return new HttpError(404, `${error.message} \u2014 ${why}`, error.body, error.retryAfter);
|
|
4454
4586
|
}
|
|
4455
|
-
async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
4587
|
+
async function explainNotGateOwner(ctx, error, gate, issue, satisfied, allowLookup) {
|
|
4456
4588
|
if (!(error instanceof HttpError) || error.status !== 403 || error.body["reason"] !== "not_gate_owner") return error;
|
|
4457
4589
|
let current;
|
|
4458
4590
|
let currentOwner = error.body["owner"];
|
|
4459
|
-
if (currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4591
|
+
if (allowLookup && currentOwner !== null && !isRecord5(currentOwner)) {
|
|
4460
4592
|
try {
|
|
4461
4593
|
const fresh = (await ctx.client.get(`/api/v1/issues/${issue}/conditions`)).data;
|
|
4462
4594
|
if (Array.isArray(fresh)) current = fresh.filter(isRecord5).find((condition) => condition["id"] === gate["id"]);
|
|
@@ -4473,23 +4605,17 @@ async function explainNotGateOwner(ctx, error, gate, issue, satisfied) {
|
|
|
4473
4605
|
var gateLabel = (condition) => str7(condition["label"]).trim();
|
|
4474
4606
|
var gateKey = (condition) => str7(condition["key"]);
|
|
4475
4607
|
function exactMatches(conditions, wanted) {
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
if (condition["kind"] !== "gate") return false;
|
|
4480
|
-
const key = gateKey(condition);
|
|
4481
|
-
return key !== "" && key === (id ?? wanted) || gateLabel(condition) === wanted;
|
|
4482
|
-
});
|
|
4608
|
+
return conditions.filter(
|
|
4609
|
+
(condition) => condition["kind"] === "gate" && (gateKey(condition) !== "" && gateKey(condition) === wanted || gateLabel(condition) === wanted)
|
|
4610
|
+
);
|
|
4483
4611
|
}
|
|
4484
4612
|
function ambiguous(conditions, issue, wanted, matches) {
|
|
4485
4613
|
const listed = matches.map((condition) => {
|
|
4486
4614
|
const what = condition["kind"] === "gate" ? `gate ${gateName({ label: gateLabel(condition), key: gateKey(condition) })}` : `${clean(condition["kind"])} condition`;
|
|
4487
4615
|
return `${clean(condition["id"])} (${what})`;
|
|
4488
4616
|
}).join(", ");
|
|
4489
|
-
const unnameable = matches.map((condition) => str7(condition["id"])).filter((id) => exactMatches(conditions, id).length !== 1).map((id) => clean(id));
|
|
4490
|
-
const caveat = unnameable.length === 0 ? "" : ` (${unnameable.join(", ")} ${unnameable.length === 1 ? "is" : "are"} also another gate's key or label, so ${unnameable.length === 1 ? "it" : "they"} cannot be named by id)`;
|
|
4491
4617
|
return new CommandError(
|
|
4492
|
-
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean
|
|
4618
|
+
`several gates on ${issue} match "${wanted}": ${listed} \u2014 pass the condition id of the one you mean`
|
|
4493
4619
|
);
|
|
4494
4620
|
}
|
|
4495
4621
|
function findGate(conditions, issue, wanted, expert = false) {
|
|
@@ -4497,11 +4623,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4497
4623
|
if (exact.length > 1) throw ambiguous(conditions, issue, wanted, exact);
|
|
4498
4624
|
if (exact.length === 1) {
|
|
4499
4625
|
const only = exact[0];
|
|
4500
|
-
if (only["kind"] !== "gate") {
|
|
4501
|
-
throw new CommandError(
|
|
4502
|
-
`condition ${wanted} on ${issue} is a ${clean(only["kind"])} condition \u2014 only a gate is satisfied by hand`
|
|
4503
|
-
);
|
|
4504
|
-
}
|
|
4505
4626
|
return only;
|
|
4506
4627
|
}
|
|
4507
4628
|
const gates = conditions.filter((condition) => condition["kind"] === "gate");
|
|
@@ -4511,7 +4632,6 @@ function findGate(conditions, issue, wanted, expert = false) {
|
|
|
4511
4632
|
);
|
|
4512
4633
|
if (loose.length > 1) throw ambiguous(conditions, issue, wanted, loose);
|
|
4513
4634
|
if (loose.length === 1) return loose[0];
|
|
4514
|
-
if (isUuid(wanted)) throw new CommandError(`No condition ${wanted} on ${issue}${expert ? " that you own" : ""}.`);
|
|
4515
4635
|
const none = expert ? `No gate of yours on ${issue}` : `No gate on ${issue}`;
|
|
4516
4636
|
if (gates.length === 0) {
|
|
4517
4637
|
throw new CommandError(`${none} matches "${wanted}" \u2014 ${expert ? "you own no gate on it" : "it has no gates"}`);
|
|
@@ -4544,6 +4664,7 @@ function isRecord6(value) {
|
|
|
4544
4664
|
}
|
|
4545
4665
|
|
|
4546
4666
|
// src/core/credentials.ts
|
|
4667
|
+
import { createHash } from "crypto";
|
|
4547
4668
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4548
4669
|
import { homedir } from "os";
|
|
4549
4670
|
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
@@ -4552,6 +4673,15 @@ function defaultCredentialsPath(env = process.env) {
|
|
|
4552
4673
|
const base = xdg !== void 0 && xdg !== "" && isAbsolute(xdg) ? xdg : join2(homedir(), ".config");
|
|
4553
4674
|
return join2(base, "harness", "credentials.json");
|
|
4554
4675
|
}
|
|
4676
|
+
function orgCredentialsPath(path2, baseUrl, org) {
|
|
4677
|
+
const url = new URL(baseUrl);
|
|
4678
|
+
const host = url.origin + url.pathname.replace(/\/+$/, "");
|
|
4679
|
+
const key = createHash("sha256").update(JSON.stringify([path2, host, org.toLowerCase()])).digest("hex");
|
|
4680
|
+
return join2(dirname2(path2), `credentials-org-${key}.json`);
|
|
4681
|
+
}
|
|
4682
|
+
function validOrgId(org) {
|
|
4683
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(org);
|
|
4684
|
+
}
|
|
4555
4685
|
function readCredentials(path2) {
|
|
4556
4686
|
try {
|
|
4557
4687
|
const parsed = JSON.parse(readFileSync3(path2, "utf8"));
|
|
@@ -4666,6 +4796,10 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4666
4796
|
const envUrl = blankToNull(env["HARNESS_API_URL"]);
|
|
4667
4797
|
const envToken = blankToNull(env["HARNESS_API_TOKEN"]);
|
|
4668
4798
|
const repoUrl = repo?.url?.replace(/\/+$/, "") ?? null;
|
|
4799
|
+
const org = envToken === null ? repo?.org?.toLowerCase() : void 0;
|
|
4800
|
+
if (org !== void 0 && !validOrgId(org)) {
|
|
4801
|
+
return { kind: "error", message: `repo org in ${repo?.path} must be an organization UUID`, warnings };
|
|
4802
|
+
}
|
|
4669
4803
|
let rawUrl;
|
|
4670
4804
|
let hostSource;
|
|
4671
4805
|
if (envUrl !== null) {
|
|
@@ -4684,6 +4818,9 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4684
4818
|
} else if (repoUrl !== null) {
|
|
4685
4819
|
rawUrl = repoUrl;
|
|
4686
4820
|
hostSource = "repo-config";
|
|
4821
|
+
} else if (org !== void 0) {
|
|
4822
|
+
rawUrl = DEFAULT_BASE_URL;
|
|
4823
|
+
hostSource = "default";
|
|
4687
4824
|
} else if (stored !== null) {
|
|
4688
4825
|
rawUrl = stored.url;
|
|
4689
4826
|
hostSource = "stored-login";
|
|
@@ -4694,13 +4831,16 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4694
4831
|
const validated = validateBaseUrl(rawUrl);
|
|
4695
4832
|
if (!validated.ok) return { kind: "error", message: validated.message, warnings };
|
|
4696
4833
|
const baseUrl = rawUrl.replace(/\/+$/, "");
|
|
4834
|
+
const selectedPath = org === void 0 ? path2 : orgCredentialsPath(path2, baseUrl, org);
|
|
4835
|
+
const selected = org === void 0 ? stored : readCredentials(selectedPath);
|
|
4697
4836
|
let token;
|
|
4698
4837
|
if (envToken !== null) {
|
|
4699
4838
|
token = envToken;
|
|
4700
|
-
} else if (
|
|
4701
|
-
token =
|
|
4839
|
+
} else if (selected !== null && normalize(selected.url) !== null && normalize(selected.url) === normalize(baseUrl)) {
|
|
4840
|
+
token = selected.token;
|
|
4702
4841
|
} else {
|
|
4703
|
-
const result = { kind: "needs-login", baseUrl, hostSource, warnings };
|
|
4842
|
+
const result = { kind: "needs-login", baseUrl, hostSource, credentialsPath: selectedPath, warnings };
|
|
4843
|
+
if (org !== void 0) result.org = org;
|
|
4704
4844
|
if (hostSource === "repo-config" && repo?.path !== void 0) result.repoConfigPath = repo.path;
|
|
4705
4845
|
return result;
|
|
4706
4846
|
}
|
|
@@ -4714,7 +4854,8 @@ function resolveConfig(env = process.env, credentialsPath, repo) {
|
|
|
4714
4854
|
// the --team flag at the command layer) stays authoritative.
|
|
4715
4855
|
team: blankToNull(env["HARNESS_TEAM"]) ?? repo?.team ?? null,
|
|
4716
4856
|
cacheDir: resolveCacheDir(env),
|
|
4717
|
-
hostSource
|
|
4857
|
+
hostSource,
|
|
4858
|
+
credentialsPath: selectedPath
|
|
4718
4859
|
},
|
|
4719
4860
|
warnings
|
|
4720
4861
|
};
|
|
@@ -4753,10 +4894,11 @@ function validateBaseUrl(baseUrl) {
|
|
|
4753
4894
|
}
|
|
4754
4895
|
if (parsed.protocol === "https:" && parsed.hostname !== "") return { ok: true };
|
|
4755
4896
|
if (parsed.protocol === "http:") {
|
|
4756
|
-
|
|
4897
|
+
const host = parsed.hostname;
|
|
4898
|
+
if (["localhost", "127.0.0.1", "[::1]"].includes(host) || host.endsWith(".localhost")) return { ok: true };
|
|
4757
4899
|
return {
|
|
4758
4900
|
ok: false,
|
|
4759
|
-
message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost only.`
|
|
4901
|
+
message: `refusing plain-http HARNESS_API_URL (${baseUrl}) \u2014 the API token would travel in cleartext. Use https, or http against localhost, *.localhost, 127.0.0.1 or [::1] only.`
|
|
4760
4902
|
};
|
|
4761
4903
|
}
|
|
4762
4904
|
return { ok: false, message: `HARNESS_API_URL must be an http(s) URL with a host, got: ${baseUrl}` };
|
|
@@ -4913,17 +5055,23 @@ export {
|
|
|
4913
5055
|
PLAN_EXECUTORS,
|
|
4914
5056
|
CONDITION_KINDS,
|
|
4915
5057
|
GATE_OWNER_ROLES,
|
|
5058
|
+
PLAN_STATUSES,
|
|
5059
|
+
PLAN_LIST_MAX_LIMIT,
|
|
4916
5060
|
readPlanFile,
|
|
4917
5061
|
planCreate,
|
|
5062
|
+
requirePlanListArgs,
|
|
5063
|
+
planList,
|
|
4918
5064
|
planShow,
|
|
4919
5065
|
planEdit,
|
|
4920
5066
|
planRelease,
|
|
4921
5067
|
planHold,
|
|
4922
5068
|
gateSet,
|
|
4923
5069
|
conditionList,
|
|
4924
|
-
|
|
5070
|
+
requirePlanRef,
|
|
4925
5071
|
assignments,
|
|
4926
5072
|
defaultCredentialsPath,
|
|
5073
|
+
orgCredentialsPath,
|
|
5074
|
+
validOrgId,
|
|
4927
5075
|
readCredentials,
|
|
4928
5076
|
writeCredentials,
|
|
4929
5077
|
deleteCredentials,
|