spur-board 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.mjs +128 -32
- package/package.json +1 -1
package/dist/daemon.mjs
CHANGED
|
@@ -13250,7 +13250,7 @@ var GithubIssuesHttpError = class extends AdapterError {
|
|
|
13250
13250
|
this.status = status;
|
|
13251
13251
|
}
|
|
13252
13252
|
};
|
|
13253
|
-
function object$
|
|
13253
|
+
function object$3(value) {
|
|
13254
13254
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
13255
13255
|
throw new AdapterError("transient", "Malformed GitHub issue response");
|
|
13256
13256
|
}
|
|
@@ -13268,14 +13268,14 @@ function issueNumber(ref, repo) {
|
|
|
13268
13268
|
return number(Number(match[2]));
|
|
13269
13269
|
}
|
|
13270
13270
|
function normalize(value, trackerId, repo, inFilter) {
|
|
13271
|
-
const issue = object$
|
|
13271
|
+
const issue = object$3(value);
|
|
13272
13272
|
if (issue["pull_request"] !== void 0) throw new AdapterError("transient", "A pull request cannot be a tracker ticket");
|
|
13273
13273
|
const updatedAt = new Date(string$3(issue["updated_at"]));
|
|
13274
13274
|
if (Number.isNaN(updatedAt.getTime())) throw new AdapterError("transient", "Malformed GitHub issue timestamp");
|
|
13275
13275
|
const labels = issue["labels"];
|
|
13276
13276
|
if (!Array.isArray(labels)) throw new AdapterError("transient", "Malformed GitHub issue labels");
|
|
13277
|
-
const assignee = issue["assignee"] == null ? null : string$3(object$
|
|
13278
|
-
const milestone = issue["milestone"] == null ? null : string$3(object$
|
|
13277
|
+
const assignee = issue["assignee"] == null ? null : string$3(object$3(issue["assignee"])["login"]);
|
|
13278
|
+
const milestone = issue["milestone"] == null ? null : string$3(object$3(issue["milestone"])["title"]);
|
|
13279
13279
|
return {
|
|
13280
13280
|
trackerId,
|
|
13281
13281
|
id: string$3(issue["node_id"]),
|
|
@@ -13284,7 +13284,7 @@ function normalize(value, trackerId, repo, inFilter) {
|
|
|
13284
13284
|
status: string$3(issue["state"]),
|
|
13285
13285
|
assignee,
|
|
13286
13286
|
sprint: milestone,
|
|
13287
|
-
labels: labels.map((label) => string$3(object$
|
|
13287
|
+
labels: labels.map((label) => string$3(object$3(label)["name"])),
|
|
13288
13288
|
inFilter,
|
|
13289
13289
|
url: string$3(issue["html_url"]),
|
|
13290
13290
|
updatedAt: updatedAt.toISOString()
|
|
@@ -13293,9 +13293,9 @@ function normalize(value, trackerId, repo, inFilter) {
|
|
|
13293
13293
|
function matchesFilter(issue, tracker) {
|
|
13294
13294
|
if (tracker.label !== void 0) {
|
|
13295
13295
|
const labels = issue["labels"];
|
|
13296
|
-
return Array.isArray(labels) && labels.some((label) => object$
|
|
13296
|
+
return Array.isArray(labels) && labels.some((label) => object$3(label)["name"] === tracker.label);
|
|
13297
13297
|
}
|
|
13298
|
-
return issue["milestone"] != null && object$
|
|
13298
|
+
return issue["milestone"] != null && object$3(issue["milestone"])["title"] === tracker.milestone;
|
|
13299
13299
|
}
|
|
13300
13300
|
/** Lists tracker issues and resolves manually included keys outside the filter. */
|
|
13301
13301
|
function createGithubIssuesAdapter(options) {
|
|
@@ -13383,7 +13383,7 @@ function createGithubIssuesAdapter(options) {
|
|
|
13383
13383
|
if (error instanceof GithubIssuesHttpError && error.status === 404) throw new AdapterError("config", "GitHub Issues repository is unavailable");
|
|
13384
13384
|
throw error;
|
|
13385
13385
|
}
|
|
13386
|
-
const match = milestones.map(object$
|
|
13386
|
+
const match = milestones.map(object$3).find((entry) => entry["title"] === options.tracker.milestone);
|
|
13387
13387
|
if (match === void 0) throw new AdapterError("config", "Configured GitHub milestone is missing");
|
|
13388
13388
|
return number(match["number"]);
|
|
13389
13389
|
}
|
|
@@ -13411,7 +13411,7 @@ function createGithubIssuesAdapter(options) {
|
|
|
13411
13411
|
}
|
|
13412
13412
|
const facts = /* @__PURE__ */ new Map();
|
|
13413
13413
|
for (const value of listed) {
|
|
13414
|
-
const issue = object$
|
|
13414
|
+
const issue = object$3(value);
|
|
13415
13415
|
if (issue["pull_request"] !== void 0) continue;
|
|
13416
13416
|
const fact = normalize(issue, options.id, options.tracker.repo, true);
|
|
13417
13417
|
facts.set(number(issue["number"]), fact);
|
|
@@ -13422,7 +13422,7 @@ function createGithubIssuesAdapter(options) {
|
|
|
13422
13422
|
const issueNo = issueNumber(ref, options.tracker.repo);
|
|
13423
13423
|
let fact = facts.get(issueNo);
|
|
13424
13424
|
if (fact === void 0) try {
|
|
13425
|
-
const detail = object$
|
|
13425
|
+
const detail = object$3((await request(`${root}/issues/${String(issueNo)}`, ctx.signal)).value);
|
|
13426
13426
|
if (detail["pull_request"] !== void 0) {
|
|
13427
13427
|
entities.push({
|
|
13428
13428
|
ref,
|
|
@@ -13483,7 +13483,7 @@ var JiraHttpError = class extends AdapterError {
|
|
|
13483
13483
|
this.status = status;
|
|
13484
13484
|
}
|
|
13485
13485
|
};
|
|
13486
|
-
function object$
|
|
13486
|
+
function object$2(value) {
|
|
13487
13487
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) return value;
|
|
13488
13488
|
throw new AdapterError("transient", "Invalid Jira response");
|
|
13489
13489
|
}
|
|
@@ -13543,10 +13543,10 @@ function sprintName(value) {
|
|
|
13543
13543
|
return null;
|
|
13544
13544
|
}
|
|
13545
13545
|
function issueFacts(value, trackerId, baseUrl, sprintField, inFilter) {
|
|
13546
|
-
const issue = object$
|
|
13547
|
-
const fields = object$
|
|
13548
|
-
const status = object$
|
|
13549
|
-
const assignee = fields["assignee"] == null ? null : object$
|
|
13546
|
+
const issue = object$2(value);
|
|
13547
|
+
const fields = object$2(issue["fields"]);
|
|
13548
|
+
const status = object$2(fields["status"]);
|
|
13549
|
+
const assignee = fields["assignee"] == null ? null : object$2(fields["assignee"]);
|
|
13550
13550
|
const key = string$2(issue["key"]);
|
|
13551
13551
|
const updated = string$2(fields["updated"]);
|
|
13552
13552
|
const updatedAt = new Date(updated);
|
|
@@ -13608,14 +13608,14 @@ function createJiraAdapter(options) {
|
|
|
13608
13608
|
if (error instanceof JiraHttpError && error.status === 404) throw new AdapterError("config", `Unknown Jira project key ${key}`);
|
|
13609
13609
|
throw error;
|
|
13610
13610
|
}
|
|
13611
|
-
const names = new Set(array(result).flatMap((entry) => array(object$
|
|
13611
|
+
const names = new Set(array(result).flatMap((entry) => array(object$2(entry)["statuses"]).map((status) => string$2(object$2(status)["name"]))));
|
|
13612
13612
|
for (const name of [...tracker.doneStatuses, ...tracker.readyStatuses]) if (!names.has(name)) throw new AdapterError("config", `Unknown Jira status ${name} in project ${key}`);
|
|
13613
13613
|
}
|
|
13614
13614
|
}
|
|
13615
13615
|
async function findSprintField(tracker, signal) {
|
|
13616
13616
|
const fields = array(await request(tracker.baseUrl, "/rest/api/3/field", signal));
|
|
13617
13617
|
for (const field of fields) {
|
|
13618
|
-
const entry = object$
|
|
13618
|
+
const entry = object$2(field);
|
|
13619
13619
|
const schema = entry["schema"];
|
|
13620
13620
|
if (schema !== null && typeof schema === "object" && !Array.isArray(schema)) {
|
|
13621
13621
|
if (schema["custom"] === SPRINT_SCHEMA) return string$2(entry["id"]);
|
|
@@ -13650,7 +13650,7 @@ function createJiraAdapter(options) {
|
|
|
13650
13650
|
...token === void 0 ? {} : { nextPageToken: token }
|
|
13651
13651
|
}).toString()}` : "/rest/api/3/search/jql";
|
|
13652
13652
|
if (options.searchMethod === "GET" && new URL(path, tracker.baseUrl).href.length > MAX_GET_SEARCH_URL_LENGTH) throw new AdapterError("config", `Jira GET search URL is too long (over ${String(MAX_GET_SEARCH_URL_LENGTH)} characters)`);
|
|
13653
|
-
const result = object$
|
|
13653
|
+
const result = object$2(await request(tracker.baseUrl, path, signal, options.searchMethod === "GET" ? void 0 : body));
|
|
13654
13654
|
issues.push(...array(result["issues"]));
|
|
13655
13655
|
const next = result["nextPageToken"];
|
|
13656
13656
|
if (next == null || next === "") {
|
|
@@ -13672,7 +13672,7 @@ function createJiraAdapter(options) {
|
|
|
13672
13672
|
async function fetchById(tracker, ref, signal, beforeRequest) {
|
|
13673
13673
|
try {
|
|
13674
13674
|
beforeRequest();
|
|
13675
|
-
const issue = object$
|
|
13675
|
+
const issue = object$2(await request(tracker.baseUrl, `/rest/api/3/issue/${encodeURIComponent(ref.id)}`, signal));
|
|
13676
13676
|
if (string$2(issue["id"]) !== ref.id) throw new AdapterError("transient", "Jira returned a different ticket id");
|
|
13677
13677
|
return issue;
|
|
13678
13678
|
} catch (error) {
|
|
@@ -13698,7 +13698,7 @@ function createJiraAdapter(options) {
|
|
|
13698
13698
|
}
|
|
13699
13699
|
async function fetchPending(tracker, ref, signal) {
|
|
13700
13700
|
try {
|
|
13701
|
-
const issue = object$
|
|
13701
|
+
const issue = object$2(await request(tracker.baseUrl, `/rest/api/3/issue/${encodeURIComponent(ref.key)}`, signal));
|
|
13702
13702
|
if (string$2(issue["key"]) !== ref.key) throw new AdapterError("transient", "Jira returned a different ticket key");
|
|
13703
13703
|
return {
|
|
13704
13704
|
status: "ok",
|
|
@@ -15254,7 +15254,7 @@ async function* recoverThreads(client, request, botIdentity) {
|
|
|
15254
15254
|
}
|
|
15255
15255
|
//#endregion
|
|
15256
15256
|
//#region ../gateway-slack/src/socket.ts
|
|
15257
|
-
function object(value) {
|
|
15257
|
+
function object$1(value) {
|
|
15258
15258
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
15259
15259
|
}
|
|
15260
15260
|
function value(value) {
|
|
@@ -15287,11 +15287,11 @@ function createSocket(api, config, botIdentity) {
|
|
|
15287
15287
|
async function handle(envelope) {
|
|
15288
15288
|
if (stopping || sink === void 0) return;
|
|
15289
15289
|
const type = value(envelope.type);
|
|
15290
|
-
const body = object(envelope.body);
|
|
15290
|
+
const body = object$1(envelope.body);
|
|
15291
15291
|
if (type === void 0 || body === null) return;
|
|
15292
15292
|
let events = [];
|
|
15293
15293
|
if (type === "events_api") {
|
|
15294
|
-
const event = object(body.event);
|
|
15294
|
+
const event = object$1(body.event);
|
|
15295
15295
|
const eventType = value(event?.type);
|
|
15296
15296
|
const channel = value(event?.channel);
|
|
15297
15297
|
if (event !== null && channel !== void 0 && (eventType === "message" || eventType === "app_mention")) {
|
|
@@ -15300,8 +15300,8 @@ function createSocket(api, config, botIdentity) {
|
|
|
15300
15300
|
...event,
|
|
15301
15301
|
type: "message"
|
|
15302
15302
|
} : event;
|
|
15303
|
-
const nested = object(message.message);
|
|
15304
|
-
const previous = object(message.previous_message);
|
|
15303
|
+
const nested = object$1(message.message);
|
|
15304
|
+
const previous = object$1(message.previous_message);
|
|
15305
15305
|
const threadTs = value(message.thread_ts) ?? value(nested?.thread_ts) ?? value(previous?.thread_ts);
|
|
15306
15306
|
const messageTs = value(message.ts) ?? value(nested?.ts);
|
|
15307
15307
|
const deletedRoot = message.subtype === "message_deleted" && threadTs === value(message.deleted_ts);
|
|
@@ -15312,8 +15312,8 @@ function createSocket(api, config, botIdentity) {
|
|
|
15312
15312
|
});
|
|
15313
15313
|
}
|
|
15314
15314
|
} else if (type === "interactive" || type === "block_actions") {
|
|
15315
|
-
const actionMessage = object(body.message);
|
|
15316
|
-
const channel = value(object(body.channel)?.id);
|
|
15315
|
+
const actionMessage = object$1(body.message);
|
|
15316
|
+
const channel = value(object$1(body.channel)?.id);
|
|
15317
15317
|
const ts = value(actionMessage?.ts);
|
|
15318
15318
|
const threadTs = value(actionMessage?.thread_ts) ?? ts;
|
|
15319
15319
|
const root = channel !== void 0 && threadTs !== void 0 && threadTs !== ts ? await rootFor(api, channel, threadTs) : actionMessage;
|
|
@@ -15509,6 +15509,86 @@ function createBoardModules(ctxFor, options = {}) {
|
|
|
15509
15509
|
//#endregion
|
|
15510
15510
|
//#region ../daemon/src/doctor.ts
|
|
15511
15511
|
const TIMEOUT_MS = 5e3;
|
|
15512
|
+
const GITHUB_FIX = "Check the GitHub token and repository access.";
|
|
15513
|
+
const GITHUB_CHECKS_FIX = "Use a classic GitHub personal access token with the repo scope, authorized for organization SSO where required, to read check runs.";
|
|
15514
|
+
const GITHUB_TEAMS_FIX = "Use a classic GitHub personal access token with the repo and read:org scopes to read requested review teams.";
|
|
15515
|
+
const GITHUB_ORG_READ_SCOPES = /* @__PURE__ */ new Set([
|
|
15516
|
+
"read:org",
|
|
15517
|
+
"write:org",
|
|
15518
|
+
"admin:org"
|
|
15519
|
+
]);
|
|
15520
|
+
const GITHUB_PROBE = `query DoctorGithubPermissions($owner: String!, $repo: String!) {
|
|
15521
|
+
repository(owner: $owner, name: $repo) {
|
|
15522
|
+
pullRequests(first: 10, states: [OPEN], orderBy: {field: UPDATED_AT, direction: DESC}) {
|
|
15523
|
+
nodes {
|
|
15524
|
+
statusCheckRollup { contexts(first: 100) { nodes { ... on CheckRun { name } } } }
|
|
15525
|
+
reviewRequests(first: 100) { nodes { requestedReviewer { ... on Team { name } } } }
|
|
15526
|
+
}
|
|
15527
|
+
}
|
|
15528
|
+
defaultBranchRef { target { ... on Commit {
|
|
15529
|
+
statusCheckRollup { contexts(first: 100) { nodes { ... on CheckRun { name } } } }
|
|
15530
|
+
} } }
|
|
15531
|
+
}
|
|
15532
|
+
}`;
|
|
15533
|
+
function object(value) {
|
|
15534
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
15535
|
+
}
|
|
15536
|
+
function scopedGithubError(value) {
|
|
15537
|
+
const error = object(value);
|
|
15538
|
+
const path = error?.["path"];
|
|
15539
|
+
if (!Array.isArray(path)) return null;
|
|
15540
|
+
const fields = path.filter((part) => typeof part === "string");
|
|
15541
|
+
if (error?.["type"] === "FORBIDDEN" && fields.includes("statusCheckRollup") && fields.includes("contexts")) return GITHUB_CHECKS_FIX;
|
|
15542
|
+
if (error?.["type"] === "INSUFFICIENT_SCOPES" && fields.includes("reviewRequests") && fields.includes("requestedReviewer") && fields.includes("name")) return GITHUB_TEAMS_FIX;
|
|
15543
|
+
return null;
|
|
15544
|
+
}
|
|
15545
|
+
async function githubGraphql(ctx, fetcher, token, repo) {
|
|
15546
|
+
const [owner, name] = repo.split("/");
|
|
15547
|
+
if (!owner || !name) return null;
|
|
15548
|
+
const controller = new AbortController();
|
|
15549
|
+
const deadline = setTimeout(() => {
|
|
15550
|
+
controller.abort();
|
|
15551
|
+
}, TIMEOUT_MS);
|
|
15552
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
15553
|
+
controller.signal.addEventListener("abort", () => {
|
|
15554
|
+
reject(/* @__PURE__ */ new Error("GitHub doctor probe timed out"));
|
|
15555
|
+
}, { once: true });
|
|
15556
|
+
});
|
|
15557
|
+
try {
|
|
15558
|
+
return await ctx.logCall("doctor.github", async () => {
|
|
15559
|
+
try {
|
|
15560
|
+
return await Promise.race([(async () => {
|
|
15561
|
+
const response = await fetcher("https://api.github.com/graphql", {
|
|
15562
|
+
method: "POST",
|
|
15563
|
+
headers: {
|
|
15564
|
+
Authorization: `Bearer ${token}`,
|
|
15565
|
+
Accept: "application/vnd.github+json",
|
|
15566
|
+
"Content-Type": "application/json"
|
|
15567
|
+
},
|
|
15568
|
+
body: JSON.stringify({
|
|
15569
|
+
query: GITHUB_PROBE,
|
|
15570
|
+
variables: {
|
|
15571
|
+
owner,
|
|
15572
|
+
repo: name
|
|
15573
|
+
}
|
|
15574
|
+
}),
|
|
15575
|
+
signal: controller.signal
|
|
15576
|
+
});
|
|
15577
|
+
return {
|
|
15578
|
+
response,
|
|
15579
|
+
body: response.ok ? await response.json() : null
|
|
15580
|
+
};
|
|
15581
|
+
})(), aborted]);
|
|
15582
|
+
} catch {
|
|
15583
|
+
throw new Error("GitHub doctor probe failed");
|
|
15584
|
+
}
|
|
15585
|
+
});
|
|
15586
|
+
} catch {
|
|
15587
|
+
return null;
|
|
15588
|
+
} finally {
|
|
15589
|
+
clearTimeout(deadline);
|
|
15590
|
+
}
|
|
15591
|
+
}
|
|
15512
15592
|
function check(id, ok, fix) {
|
|
15513
15593
|
return {
|
|
15514
15594
|
id,
|
|
@@ -15578,13 +15658,29 @@ async function runDaemonDoctor(ctx, productVersion, fetcher = fetch) {
|
|
|
15578
15658
|
checks.push(check("jira.credentials", jiraOk, "Check the Jira account, API token, and project access."));
|
|
15579
15659
|
const githubToken = secrets["GITHUB_TOKEN"];
|
|
15580
15660
|
const githubRepos = new Set(config.projects.flatMap((project) => project.repos));
|
|
15581
|
-
const
|
|
15582
|
-
|
|
15661
|
+
const githubFailure = (githubToken === void 0 ? [GITHUB_FIX] : await Promise.all([...githubRepos].map(async (repo) => {
|
|
15662
|
+
const response = await request(ctx, fetcher, "github", `https://api.github.com/repos/${repo}`, { headers: {
|
|
15583
15663
|
Authorization: `Bearer ${githubToken}`,
|
|
15584
15664
|
Accept: "application/vnd.github+json"
|
|
15585
|
-
} })
|
|
15586
|
-
|
|
15587
|
-
|
|
15665
|
+
} });
|
|
15666
|
+
if (response?.ok !== true) return GITHUB_FIX;
|
|
15667
|
+
const probe = await githubGraphql(ctx, fetcher, githubToken, repo);
|
|
15668
|
+
if (probe?.response.ok !== true) return GITHUB_FIX;
|
|
15669
|
+
const payload = object(probe.body);
|
|
15670
|
+
const errors = payload?.["errors"];
|
|
15671
|
+
if (errors !== void 0) {
|
|
15672
|
+
if (!Array.isArray(errors)) return GITHUB_FIX;
|
|
15673
|
+
const scoped = errors.map(scopedGithubError).find((fix) => fix !== null);
|
|
15674
|
+
if (scoped) return scoped;
|
|
15675
|
+
if (errors.length > 0) return GITHUB_FIX;
|
|
15676
|
+
}
|
|
15677
|
+
const pullRequests = object(object(object(payload?.["data"])?.["repository"])?.["pullRequests"]);
|
|
15678
|
+
if (!Array.isArray(pullRequests?.["nodes"])) return GITHUB_FIX;
|
|
15679
|
+
const scopes = response.headers.get("x-oauth-scopes");
|
|
15680
|
+
if (scopes !== null && !scopes.split(",").some((scope) => GITHUB_ORG_READ_SCOPES.has(scope.trim()))) return GITHUB_TEAMS_FIX;
|
|
15681
|
+
return null;
|
|
15682
|
+
}))).find((failure) => failure !== null) ?? null;
|
|
15683
|
+
checks.push(check("github.credentials", githubFailure === null, githubFailure ?? GITHUB_FIX));
|
|
15588
15684
|
const spur = await request(ctx, fetcher, "spur", new URL("sessions", `${config.adapters.spur.baseUrl.replace(/\/$/, "")}/`));
|
|
15589
15685
|
checks.push(check("spur.api", spur?.ok === true, "Start or correct the configured Spur API."));
|
|
15590
15686
|
if (!slackConfigured) checks.push(warning("slack.configured", "Slack is not configured: questions addressed to you are only visible through board inbox. Run the spur-board-setup skill to configure slack.channels."));
|