tracksiy 0.6.0 → 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.
- package/README.md +1 -0
- package/dist/tracksiy.mjs +103 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ Labels: `bug`, `feature`, `question`, `blocker`. `npx tracksiy support` prints t
|
|
|
125
125
|
| `monitors list` | Every monitor of the organization with its state, including monitors no project owns. | read | `monitors_list` |
|
|
126
126
|
| `monitors declare` | Declare monitors for a project up front, idempotently: heartbeats for jobs, HTTP checks, TLS, ingest silence. | write | `monitors_declare` |
|
|
127
127
|
| `monitors delete <slug>` | Remove one of a project's monitors by the slug it was declared with. | **destructive** | `monitors_delete` |
|
|
128
|
+
| `monitors deleted` | Monitors deleted from this project: what each was, its last check, when and by whom. | read | `monitors_deleted` |
|
|
128
129
|
| `performance` | Is this project slow, and did it get slower? Transactions compared with the previous window, plus uptime. | read | `performance` |
|
|
129
130
|
| `queries` | Which database queries and outbound HTTP calls make this project slow, with N+1 detection and the routes that run them. | read | `queries` |
|
|
130
131
|
| `not-found` | Which pages answer 404, and who links to them: broken internal links first. | read | `not_found` |
|
package/dist/tracksiy.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/version.ts
|
|
7
|
-
var VERSION = true ? "0.
|
|
7
|
+
var VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
8
8
|
var USER_AGENT = `tracksiy-cli/${VERSION} (node ${process.versions.node}; ${process.platform})`;
|
|
9
9
|
|
|
10
10
|
// src/core/errors.ts
|
|
@@ -822,7 +822,7 @@ var monitorsList = {
|
|
|
822
822
|
status: { type: "string", enum: ["all", "up", "down", "never"], description: "Filter by state. Default all." },
|
|
823
823
|
type: { type: "string", enum: ["heartbeat", "get", "ping", "ssl", "ingest"], description: "Only this monitor type." }
|
|
824
824
|
},
|
|
825
|
-
output: "{summary{total, up, down, never, unalertable}, monitors:[{id, name, project, monitorType, url, upNow, lastCheck, overdue, alertable, checkInUrl}], notes[]}",
|
|
825
|
+
output: "{summary{total, up, down, never, unalertable}, monitors:[{id, name, project, monitorType, url, upNow, lastCheck, overdue, alertable, checkInUrl, downReason, lastCheckDetail{status, expectedStatus, bytes, truncated, expectedTextFound, forbiddenTextFound, error, ms}}], notes[]}",
|
|
826
826
|
text: (d) => `${kv(d.summary ?? {})}
|
|
827
827
|
|
|
828
828
|
${table(d.monitors ?? [], [
|
|
@@ -830,6 +830,7 @@ ${table(d.monitors ?? [], [
|
|
|
830
830
|
"project",
|
|
831
831
|
"monitorType",
|
|
832
832
|
["up", (r) => r.upNow == null ? "never" : r.upNow],
|
|
833
|
+
["why down", (r) => r.downReason ?? ""],
|
|
833
834
|
["last check", (r) => ago(r.lastCheck)],
|
|
834
835
|
["alertable", (r) => r.alertable]
|
|
835
836
|
])}`,
|
|
@@ -846,7 +847,7 @@ var monitorsDeclare = {
|
|
|
846
847
|
idempotent: true,
|
|
847
848
|
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/monitors" },
|
|
848
849
|
flags: {
|
|
849
|
-
heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s)." },
|
|
850
|
+
heartbeat: { type: "list", value: "<slug>[:<interval-seconds>]", description: "A job that must check in at least this often (default 86400 s). `slug=seconds` works too." },
|
|
850
851
|
get: { type: "list", value: "<slug>=<url>", description: "HTTP GET check expecting 200." },
|
|
851
852
|
ping: { type: "list", value: "<slug>=<url>", description: "Reachability check." },
|
|
852
853
|
ssl: { type: "list", value: "<slug>=<url>", description: "TLS certificate check (expiry is reported on every check)." },
|
|
@@ -871,8 +872,9 @@ var monitorsDeclare = {
|
|
|
871
872
|
if (!body) {
|
|
872
873
|
const monitors = [];
|
|
873
874
|
for (const h of i.heartbeat ?? []) {
|
|
874
|
-
const
|
|
875
|
-
|
|
875
|
+
const m = /^([^:=\s]+)(?:[:=](\d+))?$/.exec(String(h).trim());
|
|
876
|
+
if (!m) throw usageError("bad_value", `--heartbeat expects <slug>[:<interval-seconds>], got ${JSON.stringify(h)}`, "For example --heartbeat nightly-backup:93600 (or nightly-backup=93600).");
|
|
877
|
+
monitors.push(compact({ slug: m[1], type: "heartbeat", intervalSeconds: m[2] ? Number(m[2]) : void 0 }));
|
|
876
878
|
}
|
|
877
879
|
for (const type of ["get", "ping", "ssl"]) {
|
|
878
880
|
for (const item of i[type] ?? []) {
|
|
@@ -915,6 +917,24 @@ var monitorsDelete = {
|
|
|
915
917
|
async run(ctx) {
|
|
916
918
|
requireYes(ctx, `This deletes monitor ${ctx.input.slug} and its history.`);
|
|
917
919
|
return ctx.api.send("DELETE", await projectPath(ctx, `monitors/${seg(ctx.input.slug)}${ctx.input.type ? `?type=${seg(ctx.input.type)}` : ""}`));
|
|
920
|
+
},
|
|
921
|
+
output: "{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}: what was deleted; kept in `monitors deleted`."
|
|
922
|
+
};
|
|
923
|
+
var monitorsDeleted = {
|
|
924
|
+
path: ["monitors", "deleted"],
|
|
925
|
+
summary: "Monitors deleted from this project: what each was, its last check, when and by whom.",
|
|
926
|
+
context: "project",
|
|
927
|
+
effect: "read",
|
|
928
|
+
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/monitor-deletions" },
|
|
929
|
+
flags: {
|
|
930
|
+
since: { type: "string", value: TIME, description: "Only deletions after this." },
|
|
931
|
+
limit: { type: "integer", value: "<n>", description: "How many (default 50)." }
|
|
932
|
+
},
|
|
933
|
+
examples: [{ command: "tracksiy monitors deleted --since 7d", description: "What `--prune`, `monitors delete` or the dashboard removed this week." }],
|
|
934
|
+
output: "{deleted:[{id, slug, type, url, intervalSeconds, lastCheck, lastIsUp, deleted, via, actor}]}",
|
|
935
|
+
text: (d) => table(d.deleted ?? [], ["slug", "type", "deleted", ["by", (r) => r.actor ?? r.via ?? ""], ["last check", (r) => r.lastCheck ? `${r.lastCheck}${r.lastIsUp === false ? " (down)" : ""}` : "never"]]),
|
|
936
|
+
async run(ctx) {
|
|
937
|
+
return ctx.api.get(await projectPath(ctx, "monitor-deletions"), { since: since(ctx.input.since), limit: ctx.input.limit });
|
|
918
938
|
}
|
|
919
939
|
};
|
|
920
940
|
var performance = {
|
|
@@ -1054,15 +1074,16 @@ var settingsSet = {
|
|
|
1054
1074
|
flags: {
|
|
1055
1075
|
environments: { type: "list", value: "<env,...>", description: "The environments this project reports from (empty string: any)." },
|
|
1056
1076
|
"environment-policy": { type: "string", enum: ["flag", "drop"], description: "What happens to an event from another environment." },
|
|
1057
|
-
"source-root": { type: "list", value: "<path>", description: "
|
|
1058
|
-
"fingerprint-rule": { type: "list", value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
|
|
1059
|
-
"privacy-rule": { type: "list", value: "dropHeader=<name> | mask=<email|iban|card_number|tckn|jwt>,field=<path|*>", description: "Scrub before storage. Repeatable; replaces the list." },
|
|
1077
|
+
"source-root": { type: "list", split: false, value: "<path> | strip=<path>,prepend=<dir/>[,surface=<surface>]", description: "How frame paths become repo paths: a prefix to strip, or strip + prepend, optionally for one surface. Each rule is tried on the frame's abs_path first (sentry-laravel: the full server path, /home/admin/web/shop/backend/app/...), then on filename (/app/...). Repeatable; replaces the list." },
|
|
1078
|
+
"fingerprint-rule": { type: "list", split: false, value: "type=<glob>,message=<glob>,fingerprint=<name>", description: "Group matching events into one issue. Repeatable; replaces the list." },
|
|
1079
|
+
"privacy-rule": { type: "list", split: false, value: "dropHeader=<name> | mask=<email|iban|card_number|tckn|jwt>,field=<path|*>", description: "Scrub before storage. Repeatable; replaces the list." },
|
|
1060
1080
|
body: { type: "json", value: "<json|@file|->", description: "The whole declaration; overrides the flags." }
|
|
1061
1081
|
},
|
|
1062
1082
|
examples: [
|
|
1063
1083
|
{ command: "tracksiy settings set --environments production,staging --environment-policy drop", description: "local and local1 stop reaching Tracksiy." },
|
|
1064
1084
|
{ command: "tracksiy settings set --fingerprint-rule 'type=ViewException,message=syntax error*,fingerprint=blade-syntax'", description: "Every half-written template is one issue." },
|
|
1065
|
-
{ command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." }
|
|
1085
|
+
{ command: "tracksiy settings set --source-root /home/admin/web/shop/", description: "Frames carry repoPath." },
|
|
1086
|
+
{ command: "tracksiy settings set --source-root 'strip=/var/www/shop/,prepend=backend/,surface=backend'", description: "The server checkout is only the repo's backend/ directory." }
|
|
1066
1087
|
],
|
|
1067
1088
|
output: "{sourceRoots[], environments[], environmentPolicy, fingerprintRules[], privacyRules[]}",
|
|
1068
1089
|
async run(ctx) {
|
|
@@ -1071,7 +1092,7 @@ var settingsSet = {
|
|
|
1071
1092
|
if (i.body) return ctx.api.send("PUT", path, i.body);
|
|
1072
1093
|
const current = await ctx.api.get(path);
|
|
1073
1094
|
const next = {
|
|
1074
|
-
sourceRoots: i.sourceRoot
|
|
1095
|
+
sourceRoots: i.sourceRoot ? i.sourceRoot.map((r) => /^\s*strip=/.test(r) ? pairs([r], "source-root")[0] : r) : current.sourceRoots ?? [],
|
|
1075
1096
|
environments: i.environments ? i.environments.filter((e) => e) : current.environments ?? [],
|
|
1076
1097
|
environmentPolicy: i.environmentPolicy ?? current.environmentPolicy ?? "flag",
|
|
1077
1098
|
fingerprintRules: pairs(i.fingerprintRule, "fingerprint-rule") ?? current.fingerprintRules ?? [],
|
|
@@ -1087,7 +1108,7 @@ var privacyPreview = {
|
|
|
1087
1108
|
effect: "read",
|
|
1088
1109
|
endpoint: { method: "POST", path: "/api/agent/v1/projects/{org}/{project}/settings/privacy-preview" },
|
|
1089
1110
|
flags: {
|
|
1090
|
-
"privacy-rule": { type: "list", value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
|
|
1111
|
+
"privacy-rule": { type: "list", split: false, value: "dropHeader=<name> | mask=<shape>,field=<path|*>", description: "Rules to try (default: the saved ones)." }
|
|
1091
1112
|
},
|
|
1092
1113
|
examples: [{ command: "tracksiy privacy-preview --privacy-rule 'mask=email,field=request.headers.User-Agent'", description: "Would this clear the PII check?" }],
|
|
1093
1114
|
output: "{sampled, rules[{rule, events, values}], remaining[{pattern, field, events}]}",
|
|
@@ -1096,7 +1117,7 @@ var privacyPreview = {
|
|
|
1096
1117
|
return ctx.api.send("POST", await projectPath(ctx, "settings/privacy-preview"), rules ? { privacyRules: rules } : {});
|
|
1097
1118
|
}
|
|
1098
1119
|
};
|
|
1099
|
-
var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
|
|
1120
|
+
var fleet_default = [orgs, projects, overview, changes, health, monitorsList, monitorsDeclare, monitorsDelete, monitorsDeleted, performance, queries, notFound, settingsGet, settingsSet, privacyPreview];
|
|
1100
1121
|
|
|
1101
1122
|
// src/commands/issues.ts
|
|
1102
1123
|
import { writeFileSync } from "node:fs";
|
|
@@ -1169,15 +1190,17 @@ var list = {
|
|
|
1169
1190
|
frames: { type: "integer", value: "<n>", description: "In-app stack frames inlined per issue (default 3, 0 for none)." },
|
|
1170
1191
|
unclaimed: { type: "boolean", description: "Only issues nobody holds a live claim on: what you can pick up." },
|
|
1171
1192
|
cursor: { type: "string", value: "<nextCursor>", description: "Continue from the previous page's nextCursor (same filters and sort)." },
|
|
1172
|
-
"claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." }
|
|
1193
|
+
"claimed-by": { type: "string", value: "<handle>", description: "Only issues claimed by this handle." },
|
|
1194
|
+
tags: { type: "string", value: "<key,...>", description: "Not a filter: each issue carries these tag keys' most frequent values in topTags (at most 10 keys)." }
|
|
1173
1195
|
},
|
|
1174
1196
|
examples: [
|
|
1175
1197
|
{ command: "tracksiy issues list --since 24h", description: "Unresolved issues seen in the last day." },
|
|
1198
|
+
{ command: "tracksiy issues list --tags endpoint,client_error_kind --select issues.id,issues.title,issues.topTags", description: "What each mobile issue hit, without opening every one." },
|
|
1176
1199
|
{ command: "tracksiy issues list --unclaimed --limit 5", description: "Five issues no other agent is working on." },
|
|
1177
1200
|
{ command: "tracksiy issues list --status regressed --select issues.id,issues.title", description: "Only regressions, only ids and titles." },
|
|
1178
1201
|
{ command: "tracksiy issues list --first-seen-since 2h --environment production", description: "What is new in production." }
|
|
1179
1202
|
],
|
|
1180
|
-
output: "{project, issueCount, total, hasMore, nextCursor, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, frames?}]}",
|
|
1203
|
+
output: "{project, issueCount, total, hasMore, nextCursor, issues:[{id, shortId, title, culprit, level, status, count, firstSeen, lastSeen, regressedAt, timesRegressed, resolution?, staleEvents, claimedBy?, claimedUntil?, permalink, latestEvent{release, environment, platform, exceptionType}, frames?, topTags?}]}",
|
|
1181
1204
|
text: (d) => `${d.issueCount ?? d.issues?.length ?? 0} of ${d.total ?? "?"} issues${d.hasMore ? " (more: raise --limit)" : ""}
|
|
1182
1205
|
${issueRows(d.issues)}`,
|
|
1183
1206
|
async run(ctx) {
|
|
@@ -1188,7 +1211,8 @@ ${issueRows(d.issues)}`,
|
|
|
1188
1211
|
frames: ctx.input.frames,
|
|
1189
1212
|
unclaimed: ctx.input.unclaimed || void 0,
|
|
1190
1213
|
claimedBy: ctx.input.claimedBy,
|
|
1191
|
-
cursor: ctx.input.cursor
|
|
1214
|
+
cursor: ctx.input.cursor,
|
|
1215
|
+
tags: ctx.input.tags
|
|
1192
1216
|
});
|
|
1193
1217
|
}
|
|
1194
1218
|
};
|
|
@@ -1199,9 +1223,15 @@ var view = {
|
|
|
1199
1223
|
effect: "read",
|
|
1200
1224
|
endpoint: { method: "GET", path: "/api/agent/v1/projects/{org}/{project}/issues/{issue_id}" },
|
|
1201
1225
|
args: [{ name: "id", required: true, description: "Issue id (numeric, from `issues list`)." }],
|
|
1202
|
-
flags: {
|
|
1203
|
-
|
|
1204
|
-
|
|
1226
|
+
flags: {
|
|
1227
|
+
frames: { type: "integer", value: "<n>", description: "How many frames to include." },
|
|
1228
|
+
breadcrumbs: { type: "integer", value: "<n>", description: "Breadcrumbs of the latest event (default 20, at most 50, 0 for none): what happened just before it." }
|
|
1229
|
+
},
|
|
1230
|
+
examples: [
|
|
1231
|
+
{ command: "tracksiy issues view 4812", description: "Everything needed to start fixing issue 4812." },
|
|
1232
|
+
{ command: "tracksiy issues view 4812 --select latestEvent.breadcrumbs", description: "The requests and navigations that led up to it." }
|
|
1233
|
+
],
|
|
1234
|
+
output: "{id, title, culprit, level, status, count, firstSeen, lastSeen, resolution?, staleEvents, claimedBy?, latestEvent{release, environment, platform, exceptionType, exceptionValue, commit, contexts, breadcrumbs[{timestamp, type, category, level, message, data}]}, frames[{filename, function, lineno, contextLine, inApp}], tags[], eventsLast24h, eventsLast7d, activity[], recentComments[], permalink}",
|
|
1205
1235
|
text: (d) => {
|
|
1206
1236
|
const head = kv(d, ["id", "shortId", "title", "culprit", "level", "status", "count", "eventsLast24h", "eventsLast7d", "firstSeen", "lastSeen", "regressedAt", "permalink"]);
|
|
1207
1237
|
const ev = d.latestEvent ? `
|
|
@@ -1214,14 +1244,17 @@ resolved ${d.resolution.type.replace(/_/g, " ")}: ${d.resolution.release ?? d.re
|
|
|
1214
1244
|
const claim2 = d.claimedBy ? `
|
|
1215
1245
|
claimed by ${d.claimedBy} until ${d.claimedUntil}` : "";
|
|
1216
1246
|
const act = (d.activity ?? []).slice(0, 5).map((a) => ` ${ago(a.at)} ${a.action}${a.reason ? ` (${a.reason})` : ""} by ${a.actor ?? a.user ?? a.via}${a.note ? `: ${String(a.note).split("\n")[0].slice(0, 100)}` : ""}`).join("\n");
|
|
1247
|
+
const crumbs = (d.latestEvent?.breadcrumbs ?? []).slice(-8).map((b) => ` ${b.category ?? b.type ?? "-"}${b.data?.status_code ? ` ${b.data.status_code}` : ""} ${b.message ?? [b.data?.method, b.data?.url ?? b.data?.to].filter(Boolean).join(" ")}`).join("\n");
|
|
1217
1248
|
return `${head}${res}${claim2}${ev}${frames ? `
|
|
1218
1249
|
frames (* = in app):
|
|
1219
|
-
${frames}` : ""}${
|
|
1250
|
+
${frames}` : ""}${crumbs ? `
|
|
1251
|
+
breadcrumbs (last 8, oldest first):
|
|
1252
|
+
${crumbs}` : ""}${act ? `
|
|
1220
1253
|
activity:
|
|
1221
1254
|
${act}` : ""}`;
|
|
1222
1255
|
},
|
|
1223
1256
|
async run(ctx) {
|
|
1224
|
-
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames });
|
|
1257
|
+
return ctx.api.get(await projectPath(ctx, `issues/${encodeURIComponent(ctx.input.id)}`), { frames: ctx.input.frames, breadcrumbs: ctx.input.breadcrumbs });
|
|
1225
1258
|
}
|
|
1226
1259
|
};
|
|
1227
1260
|
function statusCommand(name, status2, summary, examples) {
|
|
@@ -1547,7 +1580,7 @@ var DECLARE = {
|
|
|
1547
1580
|
recipient: {
|
|
1548
1581
|
type: "list",
|
|
1549
1582
|
value: "email|<type>:<url>",
|
|
1550
|
-
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
|
|
1583
|
+
description: `Who is told. Repeatable. \`email\` alone reaches the members of the project's teams; webhook, discord, googlechat and telegram take a URL: \`<type>:<url>\` (telegram: the Bot API sendMessage URL with chat_id; peerpacket: \`peerpacket:<channel>?to=<handle>\` has Tracksiy open the ticket with its own key, for a channel opened to your organization; or https://peerpacket.com/channels/<channel>?to=<handle>&as=<author>&key=<pp_live_\u2026> with your own key -- each alert becomes a ticket for that agent, closed when its issues are resolved).`
|
|
1551
1584
|
},
|
|
1552
1585
|
delivery: { type: "string", enum: ["immediate", "digest"], description: "For --recipient: send each alert now, or batch into the digest hours. Default immediate." },
|
|
1553
1586
|
"new-issue": { type: "boolean", description: "Alert on every new issue (default true). --no-new-issue to turn off." },
|
|
@@ -1569,6 +1602,9 @@ function recipients(list5, delivery) {
|
|
|
1569
1602
|
return compact({ type, delivery });
|
|
1570
1603
|
}
|
|
1571
1604
|
if (!target) throw usageError("bad_value", `--recipient ${raw} has no target`, `Format: ${type}:<url>`);
|
|
1605
|
+
if (type === "peerpacket" && !target.includes("://") && !target.startsWith("peerpacket:")) {
|
|
1606
|
+
return compact({ type, url: `peerpacket:${target}`, delivery });
|
|
1607
|
+
}
|
|
1572
1608
|
return compact({ type, url: target, delivery });
|
|
1573
1609
|
});
|
|
1574
1610
|
}
|
|
@@ -1625,7 +1661,8 @@ var set = {
|
|
|
1625
1661
|
examples: [
|
|
1626
1662
|
{ command: "tracksiy alerts set --recipient email --threshold 50/10", description: "Email the project's team on new issues, on bursts, and on downtime." },
|
|
1627
1663
|
{ command: "tracksiy alerts set --recipient 'telegram:https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>' --delivery digest", description: "Telegram, batched into the digest hours." },
|
|
1628
|
-
{ command: "tracksiy alerts set --recipient 'peerpacket:
|
|
1664
|
+
{ command: "tracksiy alerts set --recipient 'peerpacket:shop-ops?to=shop-agent'", description: "Alerts land as PeerPacket tickets for the shop's agent, opened by Tracksiy's own key (no workspace key in the config)." },
|
|
1665
|
+
{ command: "tracksiy alerts set --recipient 'peerpacket:https://peerpacket.com/channels/shop-ops?to=shop-agent&as=tracksiy-alerts&key=pp_live_\u2026'", description: "The same with your own PeerPacket key." }
|
|
1629
1666
|
],
|
|
1630
1667
|
output: "The stored alerting state, same shape as `alerts get`.",
|
|
1631
1668
|
text: rulesText,
|
|
@@ -1790,6 +1827,7 @@ var rotate = {
|
|
|
1790
1827
|
var keys_default = [list2, set2, rotate];
|
|
1791
1828
|
|
|
1792
1829
|
// src/commands/releases.ts
|
|
1830
|
+
import { execFileSync } from "node:child_process";
|
|
1793
1831
|
var list3 = {
|
|
1794
1832
|
path: ["releases", "list"],
|
|
1795
1833
|
aliases: [["releases"]],
|
|
@@ -1843,19 +1881,57 @@ var declare = {
|
|
|
1843
1881
|
ref: { type: "string", value: "<commit>", description: "Commit or tag it was built from." },
|
|
1844
1882
|
url: { type: "string", value: "<url>", description: "Link to the release (changelog, CI run)." },
|
|
1845
1883
|
released: { type: "boolean", description: "Mark it released now (if not already). --no-released clears it." },
|
|
1846
|
-
"released-at": { type: "string", value: "<iso>", description: "Mark it released at this time." }
|
|
1884
|
+
"released-at": { type: "string", value: "<iso>", description: "Mark it released at this time." },
|
|
1885
|
+
commits: {
|
|
1886
|
+
type: "list",
|
|
1887
|
+
value: "<prev>..<head> | <sha,...>",
|
|
1888
|
+
description: "The commits this release ships, so `issues resolve --in-commit` with any of them binds here (a push is more than its HEAD). A git range is expanded with `git rev-list` in the current directory (at most 1000). Replaces the list."
|
|
1889
|
+
}
|
|
1847
1890
|
},
|
|
1848
1891
|
examples: [
|
|
1849
|
-
{ command: "tracksiy releases declare shop@$(git rev-parse --short HEAD) --ref $(git rev-parse HEAD) --released", description: "From a deploy script." }
|
|
1892
|
+
{ command: "tracksiy releases declare shop@$(git rev-parse --short HEAD) --ref $(git rev-parse HEAD) --released", description: "From a deploy script." },
|
|
1893
|
+
{ command: "tracksiy releases declare shop@$(git rev-parse HEAD) --ref $(git rev-parse HEAD) --commits $LAST_DEPLOYED..HEAD --released", description: "Every commit of the push: a fix resolved --in-commit with any of them binds to this release." }
|
|
1850
1894
|
],
|
|
1851
|
-
output: "{version, ref, url, released, created, projects[], artifacts}",
|
|
1895
|
+
output: "{version, ref, url, released, commitCount, boundResolutions, created, projects[], artifacts}",
|
|
1852
1896
|
async run(ctx) {
|
|
1853
1897
|
const i = ctx.input;
|
|
1854
1898
|
if (i.released !== void 0 && i.releasedAt) throw usageError("conflict", "Pass --released or --released-at, not both.");
|
|
1855
1899
|
if (i.releasedAt && !Number.isFinite(Date.parse(i.releasedAt))) throw usageError("bad_value", `--released-at is not a time: ${i.releasedAt}`);
|
|
1856
|
-
return ctx.api.send(
|
|
1900
|
+
return ctx.api.send(
|
|
1901
|
+
"PUT",
|
|
1902
|
+
await projectPath(ctx, `releases/${seg(i.version)}`),
|
|
1903
|
+
compact({ ref: i.ref, url: i.url, released: i.releasedAt ?? i.released, commits: i.commits ? commitList(i.commits) : void 0 })
|
|
1904
|
+
);
|
|
1857
1905
|
}
|
|
1858
1906
|
};
|
|
1907
|
+
var MAX_COMMITS = 1e3;
|
|
1908
|
+
var SHA = /^[0-9a-f]{7,40}$/i;
|
|
1909
|
+
function commitList(values, git = runGit) {
|
|
1910
|
+
const out = [];
|
|
1911
|
+
for (const raw of values) {
|
|
1912
|
+
const value = raw.trim();
|
|
1913
|
+
if (!value) continue;
|
|
1914
|
+
if (value.includes("..")) {
|
|
1915
|
+
let listed;
|
|
1916
|
+
try {
|
|
1917
|
+
listed = git(["rev-list", `--max-count=${MAX_COMMITS + 1}`, value]);
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
throw usageError("bad_value", `--commits ${value}: git rev-list failed: ${String(err?.stderr || err?.message || err).trim().split("\n")[0]}`, "Run it inside the repository, with both ends fetched (e.g. git fetch --unshallow in CI).");
|
|
1920
|
+
}
|
|
1921
|
+
out.push(...listed.split(/\s+/).filter(Boolean));
|
|
1922
|
+
} else if (SHA.test(value)) {
|
|
1923
|
+
out.push(value.toLowerCase());
|
|
1924
|
+
} else {
|
|
1925
|
+
throw usageError("bad_value", `--commits: ${JSON.stringify(value)} is neither a sha nor a <prev>..<head> range.`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
const unique = [...new Set(out.map((c) => c.toLowerCase()))];
|
|
1929
|
+
if (unique.length > MAX_COMMITS) throw usageError("bad_value", `--commits names more than ${MAX_COMMITS} commits.`, "Is the range's base the last deployed commit?");
|
|
1930
|
+
return unique;
|
|
1931
|
+
}
|
|
1932
|
+
function runGit(args) {
|
|
1933
|
+
return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
1934
|
+
}
|
|
1859
1935
|
var releases_default = [list3, health2, declare];
|
|
1860
1936
|
|
|
1861
1937
|
// src/commands/artifacts.ts
|
|
@@ -2734,7 +2810,7 @@ function coerce(name, spec, value, previous) {
|
|
|
2734
2810
|
return n;
|
|
2735
2811
|
}
|
|
2736
2812
|
case "list": {
|
|
2737
|
-
const items = Array.isArray(value) ? value.map(String) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2813
|
+
const items = Array.isArray(value) ? value.map(String) : spec.split === false ? [String(value).trim()].filter(Boolean) : String(value).split(",").map((s) => s.trim()).filter(Boolean);
|
|
2738
2814
|
if (spec.enum) for (const item of items) checkEnum(name, spec, item);
|
|
2739
2815
|
return [...previous ?? [], ...items];
|
|
2740
2816
|
}
|