standup-mr 0.6.1 → 1.0.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/CHANGELOG.md +46 -0
- package/README.md +82 -29
- package/dist/{chunk-NJO6LFYN.js → chunk-S7DAKN36.js} +147 -47
- package/dist/cli.js +19 -5
- package/dist/index.d.ts +60 -60
- package/dist/index.js +1 -1
- package/dist/mcp/server.js +221 -72
- package/llms-install.md +17 -12
- package/mcp/README.md +33 -28
- package/package.json +19 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,52 @@ All notable changes to standup-mr are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.0.0] - 2026-10-12
|
|
8
|
+
|
|
9
|
+
First stable release. From here on the CLI commands and flags, the JSON that
|
|
10
|
+
`fetch` and `get_standup_data` return, the MCP tool names and arguments, and
|
|
11
|
+
the environment variables follow semantic versioning: anything that would
|
|
12
|
+
break an existing setup waits for 2.0.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Remote MCP server for claude.ai.** Add
|
|
17
|
+
`https://standup.ilkerbalcilar.com/mcp` as a custom connector, sign in with
|
|
18
|
+
GitHub or gitlab.com — or paste a token for a self-hosted host — and use
|
|
19
|
+
standup-mr from claude.ai on the web and on mobile, with no environment
|
|
20
|
+
variables. One connector can hold several accounts and an optional chat
|
|
21
|
+
webhook. The server lives in `remote/` and is not part of the npm package.
|
|
22
|
+
- **Sign in to your company's GitLab or GitHub Enterprise.** On the connect
|
|
23
|
+
page, type the host: if someone there already set up sign-in, use it;
|
|
24
|
+
otherwise register an OAuth application once, following the exact redirect
|
|
25
|
+
URI and scopes the page shows, and everyone at that host can sign in from
|
|
26
|
+
then on.
|
|
27
|
+
- `server.json` lists the remote endpoint, so registries show both the npm
|
|
28
|
+
package and the hosted server.
|
|
29
|
+
- Both servers declare the project icon in `serverInfo.icons`.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- GitLab requests send the token as `Authorization: Bearer` instead of
|
|
34
|
+
`PRIVATE-TOKEN`. Personal access tokens work either way, but OAuth tokens —
|
|
35
|
+
from `glab`'s browser login or from the remote server's sign-in — were
|
|
36
|
+
rejected with 401.
|
|
37
|
+
|
|
38
|
+
## [0.7.0] - 2026-10-10
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Tool annotations.** Every MCP tool now declares `readOnlyHint`,
|
|
43
|
+
`destructiveHint`, `idempotentHint` and `openWorldHint`, so a host can tell
|
|
44
|
+
the read-only `get_standup_data` and `get_note_instructions` apart from
|
|
45
|
+
`post_standup_note`, which sends a message other people will see, and warn
|
|
46
|
+
before invoking it.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- The MCP server registers its tools with `registerTool`, which needs
|
|
51
|
+
`@modelcontextprotocol/sdk` 1.30.0 or later.
|
|
52
|
+
|
|
7
53
|
## [0.6.1] - 2026-09-02
|
|
8
54
|
|
|
9
55
|
### Fixed
|
package/README.md
CHANGED
|
@@ -12,13 +12,13 @@ pipeline is red, it opens the job log and tells you **why**.
|
|
|
12
12
|
|
|
13
13
|
## What makes it different
|
|
14
14
|
|
|
15
|
-
|
|
|
16
|
-
|
|
17
|
-
| Source
|
|
18
|
-
| Merge request state
|
|
19
|
-
| Review queue
|
|
20
|
-
| Failed pipeline
|
|
21
|
-
| Self-hosted (GitLab CE/EE, GitHub Enterprise) | varies
|
|
15
|
+
| | commit-log tools | standup-mr |
|
|
16
|
+
| --------------------------------------------- | ---------------- | ----------------------------------------------------- |
|
|
17
|
+
| Source | local `git log` | GitLab or GitHub API |
|
|
18
|
+
| Merge request state | ✗ | ready / blocked / draft / stale |
|
|
19
|
+
| Review queue | ✗ | pending only, approvals filtered out |
|
|
20
|
+
| Failed pipeline | ✗ | error lines from the job trace or the Actions job log |
|
|
21
|
+
| Self-hosted (GitLab CE/EE, GitHub Enterprise) | varies | first class |
|
|
22
22
|
|
|
23
23
|
## Use
|
|
24
24
|
|
|
@@ -36,10 +36,10 @@ npx standup-mr fetch --markdown | npx standup-mr post --google-chat "$URL"
|
|
|
36
36
|
|
|
37
37
|
### Identity
|
|
38
38
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
| Flags
|
|
42
|
-
| Env
|
|
39
|
+
| | GitHub | GitLab |
|
|
40
|
+
| ----------- | ------------------------------------------- | ------------------------------------------------------- |
|
|
41
|
+
| Flags | `--host` / `--token` | `--host` / `--token` |
|
|
42
|
+
| Env | `GITHUB_HOST` / `GITHUB_TOKEN` | `GITLAB_HOST` / `GITLAB_TOKEN` |
|
|
43
43
|
| CLI session | [`gh`](https://cli.github.com/) auth config | [`glab`](https://gitlab.com/gitlab-org/cli) auth config |
|
|
44
44
|
|
|
45
45
|
GitHub defaults to `github.com` when no host is given. GitLab has no default —
|
|
@@ -66,12 +66,55 @@ So if you already use `gh` or `glab`, there is nothing to configure.
|
|
|
66
66
|
npx standup-mr fetch --markdown | npx standup-mr post --slack "$SLACK_WEBHOOK_URL"
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
##
|
|
69
|
+
## claude.ai (web and mobile)
|
|
70
|
+
|
|
71
|
+
No token, no config file — sign in instead. In claude.ai open **Settings →
|
|
72
|
+
Connectors → Add custom connector** and paste:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
https://standup.ilkerbalcilar.com/mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Press **Connect**. A page opens where you link the accounts the note should
|
|
79
|
+
read:
|
|
80
|
+
|
|
81
|
+
- **GitHub** — sign in through the standup-mr GitHub App. It only sees the
|
|
82
|
+
repositories it is installed on, so install it on your org too; if the org
|
|
83
|
+
won't approve it, add github.com with a fine-grained token instead.
|
|
84
|
+
- **gitlab.com** — sign in with GitLab.
|
|
85
|
+
- **Your company's GitLab or GitHub Enterprise** — type the host. If someone
|
|
86
|
+
at your company already set up sign-in, you get a "Sign in with …" button.
|
|
87
|
+
Otherwise set it up once: the page shows the exact redirect URI and scopes
|
|
88
|
+
for an OAuth application on your host, you paste its ID and secret, and
|
|
89
|
+
everyone at that host can sign in from then on. A read-only token works too.
|
|
90
|
+
- **Chat webhook** _(optional)_ — Slack, Discord or Google Chat, so Claude can
|
|
91
|
+
post the finished note when you ask it to.
|
|
92
|
+
|
|
93
|
+
Link as many as you like — say, GitHub for side projects and your company's
|
|
94
|
+
GitLab for work — then ask for "my standup" or "my work standup". With
|
|
95
|
+
several accounts linked, Claude asks which one you mean.
|
|
96
|
+
|
|
97
|
+
The connector then works from the claude.ai mobile app as well.
|
|
98
|
+
|
|
99
|
+
**What is stored.** Your provider tokens and webhook URL, encrypted with
|
|
100
|
+
AES-256-GCM, and only to read your merge requests and post your note. The
|
|
101
|
+
access tokens Claude holds are stored as hashes. Disconnecting the connector
|
|
102
|
+
deletes everything; connections unused for 30 days are deleted. The server is
|
|
103
|
+
the code in [`remote/`](remote/).
|
|
104
|
+
|
|
105
|
+
**When to use the local server instead.** A GitLab behind a VPN or on a
|
|
106
|
+
private network is out of reach of a hosted server. Run the stdio server
|
|
107
|
+
below on your own machine — the token never leaves it.
|
|
108
|
+
|
|
109
|
+
## The four surfaces
|
|
70
110
|
|
|
71
111
|
**CLI** — the core. Emits JSON; zero runtime dependencies.
|
|
72
112
|
|
|
73
|
-
**MCP server** (`mcp/`) —
|
|
74
|
-
|
|
113
|
+
**MCP server** (`mcp/`) — three tools for Claude Desktop, Claude Code, Cursor,
|
|
114
|
+
or any MCP client that runs a local process. See [`mcp/README.md`](mcp/README.md).
|
|
115
|
+
|
|
116
|
+
**Remote MCP server** (`remote/`) — the same tools over HTTPS with sign-in, for
|
|
117
|
+
claude.ai on the web and on mobile. See [above](#claudeai-web-and-mobile).
|
|
75
118
|
|
|
76
119
|
**Claude Code plugin** — the note-writing playbook, shipped as the `standup`
|
|
77
120
|
skill. From inside Claude Code:
|
|
@@ -93,15 +136,15 @@ paste in the note-writing rules separately.
|
|
|
93
136
|
|
|
94
137
|
```json
|
|
95
138
|
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"standup": {
|
|
141
|
+
"command": "npx",
|
|
142
|
+
"args": ["-y", "standup-mr", "mcp"],
|
|
143
|
+
"env": {
|
|
144
|
+
"GITHUB_TOKEN": "ghp_..."
|
|
145
|
+
}
|
|
146
|
+
}
|
|
103
147
|
}
|
|
104
|
-
}
|
|
105
148
|
}
|
|
106
149
|
```
|
|
107
150
|
|
|
@@ -123,11 +166,11 @@ documentation for the current format rather than trusting this file blindly.
|
|
|
123
166
|
|
|
124
167
|
The server exposes three tools:
|
|
125
168
|
|
|
126
|
-
| Tool
|
|
127
|
-
|
|
128
|
-
| `get_standup_data`
|
|
169
|
+
| Tool | What it does |
|
|
170
|
+
| ----------------------- | -------------------------------------------------------------------------------------------- |
|
|
171
|
+
| `get_standup_data` | Reads the provider and returns the report as JSON. Optional `provider`, `host`, `lang`. |
|
|
129
172
|
| `get_note_instructions` | Returns the note-writing rules, so the assistant can write the note the way the skill would. |
|
|
130
|
-
| `post_standup_note`
|
|
173
|
+
| `post_standup_note` | Posts a finished note to a Slack, Discord or Google Chat webhook. |
|
|
131
174
|
|
|
132
175
|
`post_standup_note` reads the webhook URL from `STANDUP_WEBHOOK_URL` and never
|
|
133
176
|
takes it as an argument — anyone holding that URL can post to the channel, so
|
|
@@ -196,8 +239,8 @@ _Structured digest — not a written note._
|
|
|
196
239
|
## Blockers
|
|
197
240
|
|
|
198
241
|
- `acme/mobile` !6 — job `quality`
|
|
199
|
-
|
|
200
|
-
|
|
242
|
+
- `npm ERR! code E404`
|
|
243
|
+
- `npm ERR! 404 Not Found - GET https://registry.example.com/@acme%2fui`
|
|
201
244
|
```
|
|
202
245
|
|
|
203
246
|
The last section is the point of the tool. Every other standup tool can tell you
|
|
@@ -205,6 +248,16 @@ that pipeline is red; this one opens the failed job's log and shows you the
|
|
|
205
248
|
404 — and a 404 rather than a 403 usually means the token's scope is wrong, not
|
|
206
249
|
that the package is missing.
|
|
207
250
|
|
|
251
|
+
## Stability
|
|
252
|
+
|
|
253
|
+
Since 1.0.0 the CLI commands and flags, the JSON that `fetch` and
|
|
254
|
+
`get_standup_data` return, the MCP tool names and arguments, and the
|
|
255
|
+
environment variables follow [semantic versioning](https://semver.org/): a
|
|
256
|
+
change that would break an existing setup waits for a major release. New
|
|
257
|
+
fields in the JSON may appear in minor releases, so read it by name rather
|
|
258
|
+
than by position. The hosted server at `standup.ilkerbalcilar.com` is
|
|
259
|
+
updated continuously and is not tied to the npm version.
|
|
260
|
+
|
|
208
261
|
## Known limits
|
|
209
262
|
|
|
210
263
|
- **GitHub's events feed is shallow.** It is capped at roughly 300 events over
|
|
@@ -232,7 +285,7 @@ pipe `standup fetch` into anything, or import the package, read
|
|
|
232
285
|
returns `null` with no error.
|
|
233
286
|
- `MergeRequest`, `Review` and `Blocker` carry a required `provider` field, and
|
|
234
287
|
`Provider.getReviews` takes an `Identity` rather than a numeric id.
|
|
235
|
-
- The MCP `CollectOptions.provider` is now a provider
|
|
288
|
+
- The MCP `CollectOptions.provider` is now a provider _name_; inject a
|
|
236
289
|
`Provider` instance through `providerImpl`.
|
|
237
290
|
|
|
238
291
|
Claude Code plugin users should run `/plugin marketplace update standup-mr` —
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/buckets/buckets.constants.ts
|
|
4
|
-
var STALE_DAYS = 14;
|
|
5
|
-
|
|
6
3
|
// src/dates/dates.constants.ts
|
|
7
4
|
var DAYS = {
|
|
8
|
-
en: [
|
|
9
|
-
|
|
5
|
+
en: [
|
|
6
|
+
"Sunday",
|
|
7
|
+
"Monday",
|
|
8
|
+
"Tuesday",
|
|
9
|
+
"Wednesday",
|
|
10
|
+
"Thursday",
|
|
11
|
+
"Friday",
|
|
12
|
+
"Saturday"
|
|
13
|
+
],
|
|
14
|
+
tr: [
|
|
15
|
+
"Pazar",
|
|
16
|
+
"Pazartesi",
|
|
17
|
+
"Sal\u0131",
|
|
18
|
+
"\xC7ar\u015Famba",
|
|
19
|
+
"Per\u015Fembe",
|
|
20
|
+
"Cuma",
|
|
21
|
+
"Cumartesi"
|
|
22
|
+
]
|
|
10
23
|
};
|
|
11
24
|
var MONTHS = {
|
|
12
25
|
en: [
|
|
@@ -77,10 +90,14 @@ function previousActiveDays(eventDates, today) {
|
|
|
77
90
|
}));
|
|
78
91
|
}
|
|
79
92
|
|
|
93
|
+
// src/buckets/buckets.constants.ts
|
|
94
|
+
var STALE_DAYS = 14;
|
|
95
|
+
|
|
80
96
|
// src/buckets/buckets.ts
|
|
81
97
|
function classify(mr, today, staleDays = STALE_DAYS) {
|
|
82
98
|
if (mr.draft) return "draft";
|
|
83
|
-
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
99
|
+
if (mr.pipeline === "failed" || mr.pipeline === "canceled" || mr.unresolved > 0)
|
|
100
|
+
return "blocked";
|
|
84
101
|
const age = Math.round(
|
|
85
102
|
(Date.parse(`${isoDay(today)}T00:00:00Z`) - Date.parse(`${mr.updated}T00:00:00Z`)) / MS_PER_DAY
|
|
86
103
|
);
|
|
@@ -169,7 +186,9 @@ function cliStatus(cli) {
|
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
function parseLoggedInHosts(statusOutput) {
|
|
172
|
-
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
189
|
+
const hosts = [...statusOutput.matchAll(/Logged in to (\S+)/g)].map(
|
|
190
|
+
(match) => match[1]
|
|
191
|
+
);
|
|
173
192
|
return [...new Set(hosts)].sort();
|
|
174
193
|
}
|
|
175
194
|
var parseGlabHosts = parseLoggedInHosts;
|
|
@@ -186,14 +205,6 @@ function ghToken(host) {
|
|
|
186
205
|
return cliCapture("gh", ["auth", "token", "--hostname", host]);
|
|
187
206
|
}
|
|
188
207
|
|
|
189
|
-
// src/notify/notify.constants.ts
|
|
190
|
-
var PAYLOAD_FIELD = {
|
|
191
|
-
slack: "text",
|
|
192
|
-
discord: "content",
|
|
193
|
-
"google-chat": "text"
|
|
194
|
-
};
|
|
195
|
-
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
196
|
-
|
|
197
208
|
// src/render/chat.ts
|
|
198
209
|
var MARKDOWN_NATIVE = ["discord"];
|
|
199
210
|
var FENCE = /^```/;
|
|
@@ -214,6 +225,14 @@ function toChatText(markdown, kind) {
|
|
|
214
225
|
}).join("\n");
|
|
215
226
|
}
|
|
216
227
|
|
|
228
|
+
// src/notify/notify.constants.ts
|
|
229
|
+
var PAYLOAD_FIELD = {
|
|
230
|
+
"slack": "text",
|
|
231
|
+
"discord": "content",
|
|
232
|
+
"google-chat": "text"
|
|
233
|
+
};
|
|
234
|
+
var WEBHOOK_KINDS = Object.keys(PAYLOAD_FIELD);
|
|
235
|
+
|
|
217
236
|
// src/notify/notify.ts
|
|
218
237
|
async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
219
238
|
const field = PAYLOAD_FIELD[kind];
|
|
@@ -228,7 +247,9 @@ async function postWebhook(url, text, kind = "slack", fetchImpl = fetch) {
|
|
|
228
247
|
body: JSON.stringify({ [field]: toChatText(text, kind) })
|
|
229
248
|
});
|
|
230
249
|
if (!response.ok) {
|
|
231
|
-
throw new Error(
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Webhook rejected the message: HTTP ${response.status}.`
|
|
252
|
+
);
|
|
232
253
|
}
|
|
233
254
|
}
|
|
234
255
|
var WEBHOOK_HOSTS = {
|
|
@@ -279,17 +300,25 @@ function retryAfter(response) {
|
|
|
279
300
|
const value = response.headers.get("retry-after");
|
|
280
301
|
if (!value) return "";
|
|
281
302
|
const seconds = Number(value);
|
|
282
|
-
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
303
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
304
|
+
return `; retry after ${value}`;
|
|
283
305
|
return `; retry after ${seconds}s`;
|
|
284
306
|
}
|
|
285
307
|
function assertUsable(response, host) {
|
|
286
308
|
if (response.ok || response.status === 404) return;
|
|
287
309
|
if (response.status === 403 || response.status === 429) {
|
|
288
310
|
if (remaining(response) === "0") {
|
|
289
|
-
throw new ApiError(
|
|
311
|
+
throw new ApiError(
|
|
312
|
+
`${host} rate limit reached${resetAt(response)}.`,
|
|
313
|
+
response.status
|
|
314
|
+
);
|
|
290
315
|
}
|
|
291
316
|
const retry = retryAfter(response);
|
|
292
|
-
if (retry)
|
|
317
|
+
if (retry)
|
|
318
|
+
throw new ApiError(
|
|
319
|
+
`${host} rate limit reached${retry}.`,
|
|
320
|
+
response.status
|
|
321
|
+
);
|
|
293
322
|
}
|
|
294
323
|
if (response.status === 401) {
|
|
295
324
|
throw new ApiError(
|
|
@@ -490,8 +519,8 @@ var GitHubProvider = class {
|
|
|
490
519
|
}
|
|
491
520
|
headers() {
|
|
492
521
|
return {
|
|
493
|
-
authorization: `Bearer ${this.token}`,
|
|
494
|
-
accept: "application/vnd.github+json",
|
|
522
|
+
"authorization": `Bearer ${this.token}`,
|
|
523
|
+
"accept": "application/vnd.github+json",
|
|
495
524
|
"x-github-api-version": API_VERSION
|
|
496
525
|
};
|
|
497
526
|
}
|
|
@@ -543,11 +572,14 @@ var GitHubProvider = class {
|
|
|
543
572
|
return rows;
|
|
544
573
|
}
|
|
545
574
|
async getLogText(path) {
|
|
546
|
-
const first = await this.send(buildUrl(this.api, path), {
|
|
575
|
+
const first = await this.send(buildUrl(this.api, path), {
|
|
576
|
+
redirect: "manual"
|
|
577
|
+
});
|
|
547
578
|
if (first.status === 404) return "";
|
|
548
579
|
const location = first.headers.get("location");
|
|
549
580
|
if (!location) {
|
|
550
|
-
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
581
|
+
if (first.status === 0 || first.status >= 300 && first.status < 400)
|
|
582
|
+
return "";
|
|
551
583
|
assertUsable(first, this.host);
|
|
552
584
|
return first.ok ? await first.text() : "";
|
|
553
585
|
}
|
|
@@ -590,7 +622,9 @@ var GitHubProvider = class {
|
|
|
590
622
|
async shapePr(item) {
|
|
591
623
|
const project = repoFromUrl(item.repository_url);
|
|
592
624
|
const iid = item.number;
|
|
593
|
-
const pull = await this.getJson(
|
|
625
|
+
const pull = await this.getJson(
|
|
626
|
+
`repos/${project}/pulls/${iid}`
|
|
627
|
+
);
|
|
594
628
|
if (!pull) return null;
|
|
595
629
|
const sha = pull.head?.sha ?? "";
|
|
596
630
|
const [checks, reviews] = await Promise.all([
|
|
@@ -598,9 +632,15 @@ var GitHubProvider = class {
|
|
|
598
632
|
`repos/${project}/commits/${sha}/check-runs`,
|
|
599
633
|
{ per_page: PAGE_SIZE }
|
|
600
634
|
) : Promise.resolve(null),
|
|
601
|
-
this.getPaged(
|
|
635
|
+
this.getPaged(
|
|
636
|
+
`repos/${project}/pulls/${iid}/reviews`,
|
|
637
|
+
{},
|
|
638
|
+
2
|
|
639
|
+
)
|
|
602
640
|
]);
|
|
603
|
-
const { pipeline, pipelineId } = normalizeChecks(
|
|
641
|
+
const { pipeline, pipelineId } = normalizeChecks(
|
|
642
|
+
checks?.check_runs ?? []
|
|
643
|
+
);
|
|
604
644
|
return {
|
|
605
645
|
provider: "github",
|
|
606
646
|
project,
|
|
@@ -639,7 +679,9 @@ var GitHubProvider = class {
|
|
|
639
679
|
`is:pr is:open review-requested:${identity.username}`,
|
|
640
680
|
SEARCH_CAP
|
|
641
681
|
);
|
|
642
|
-
const cutoff = isoDay(
|
|
682
|
+
const cutoff = isoDay(
|
|
683
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS * MS_PER_DAY)
|
|
684
|
+
);
|
|
643
685
|
const rows = await Promise.all(
|
|
644
686
|
items.map(async (item) => {
|
|
645
687
|
const project = repoFromUrl(item.repository_url);
|
|
@@ -705,7 +747,10 @@ var GitHubProvider = class {
|
|
|
705
747
|
(run) => run.conclusion !== null && FAILED_CONCLUSIONS.has(run.conclusion)
|
|
706
748
|
);
|
|
707
749
|
if (!failed) return null;
|
|
708
|
-
const summary = [
|
|
750
|
+
const summary = [
|
|
751
|
+
failed.output?.summary ?? "",
|
|
752
|
+
failed.output?.text ?? ""
|
|
753
|
+
].filter(Boolean).join("\n");
|
|
709
754
|
const errors = extractErrors(summary);
|
|
710
755
|
if (errors.length === 0) return null;
|
|
711
756
|
return {
|
|
@@ -765,7 +810,10 @@ var GitLabProvider = class {
|
|
|
765
810
|
const response = await sendWithRetry(
|
|
766
811
|
this.fetchImpl,
|
|
767
812
|
url,
|
|
768
|
-
|
|
813
|
+
// Bearer, not PRIVATE-TOKEN: GitLab accepts personal access tokens
|
|
814
|
+
// either way, but OAuth tokens — glab's browser login, and the
|
|
815
|
+
// remote server's sign-in — only as a bearer.
|
|
816
|
+
{ headers: { Authorization: `Bearer ${this.token}` } },
|
|
769
817
|
this.host
|
|
770
818
|
);
|
|
771
819
|
assertUsable(response, this.host);
|
|
@@ -818,7 +866,9 @@ var GitLabProvider = class {
|
|
|
818
866
|
const raw = await this.getPaged("events", {
|
|
819
867
|
after: isoDay(since)
|
|
820
868
|
});
|
|
821
|
-
const ids = [
|
|
869
|
+
const ids = [
|
|
870
|
+
...new Set(raw.map((e) => e.project_id).filter(Boolean))
|
|
871
|
+
];
|
|
822
872
|
const paths = new Map(
|
|
823
873
|
await Promise.all(
|
|
824
874
|
ids.map(async (id) => [id, await this.projectPath(id)])
|
|
@@ -829,7 +879,7 @@ var GitLabProvider = class {
|
|
|
829
879
|
return {
|
|
830
880
|
at: String(event.created_at).slice(0, 16),
|
|
831
881
|
action: event.action_name,
|
|
832
|
-
project: paths.get(event.project_id) ?? "",
|
|
882
|
+
project: paths.get(event.project_id ?? -1) ?? "",
|
|
833
883
|
targetType: event.target_type ?? "",
|
|
834
884
|
title: event.target_title ?? "",
|
|
835
885
|
branch: push.ref ?? "",
|
|
@@ -841,7 +891,9 @@ var GitLabProvider = class {
|
|
|
841
891
|
async countUnresolved(projectId, iid) {
|
|
842
892
|
const discussions = await this.getJson(
|
|
843
893
|
`projects/${projectId}/merge_requests/${iid}/discussions`,
|
|
844
|
-
{
|
|
894
|
+
{
|
|
895
|
+
per_page: PAGE_SIZE2
|
|
896
|
+
}
|
|
845
897
|
);
|
|
846
898
|
if (!discussions) return 0;
|
|
847
899
|
return discussions.filter((discussion) => {
|
|
@@ -894,7 +946,9 @@ var GitLabProvider = class {
|
|
|
894
946
|
async approvedByMe(projectId, iid, uid) {
|
|
895
947
|
const approvals = await this.getJson(`projects/${projectId}/merge_requests/${iid}/approvals`);
|
|
896
948
|
if (!approvals) return false;
|
|
897
|
-
return (approvals.approved_by ?? []).some(
|
|
949
|
+
return (approvals.approved_by ?? []).some(
|
|
950
|
+
(entry) => entry.user?.id === uid
|
|
951
|
+
);
|
|
898
952
|
}
|
|
899
953
|
async getReviews(identity, today) {
|
|
900
954
|
const raw = await this.getPaged("merge_requests", {
|
|
@@ -902,7 +956,9 @@ var GitLabProvider = class {
|
|
|
902
956
|
state: "opened",
|
|
903
957
|
reviewer_id: identity.id
|
|
904
958
|
});
|
|
905
|
-
const cutoff = isoDay(
|
|
959
|
+
const cutoff = isoDay(
|
|
960
|
+
new Date(today.getTime() - FRESH_REVIEW_DAYS2 * MS_PER_DAY)
|
|
961
|
+
);
|
|
906
962
|
const rows = await Promise.all(
|
|
907
963
|
raw.map(async (mr) => ({
|
|
908
964
|
provider: "gitlab",
|
|
@@ -914,7 +970,11 @@ var GitLabProvider = class {
|
|
|
914
970
|
draft: mr.draft,
|
|
915
971
|
url: mr.web_url,
|
|
916
972
|
fresh: String(mr.updated_at).slice(0, 10) >= cutoff,
|
|
917
|
-
approvedByMe: await this.approvedByMe(
|
|
973
|
+
approvedByMe: await this.approvedByMe(
|
|
974
|
+
mr.project_id,
|
|
975
|
+
mr.iid,
|
|
976
|
+
identity.id
|
|
977
|
+
)
|
|
918
978
|
}))
|
|
919
979
|
);
|
|
920
980
|
return rows.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
@@ -926,7 +986,9 @@ var GitLabProvider = class {
|
|
|
926
986
|
) ?? [];
|
|
927
987
|
const job = jobs.find((j) => j.status === "failed");
|
|
928
988
|
if (!job) return null;
|
|
929
|
-
const trace = await this.getText(
|
|
989
|
+
const trace = await this.getText(
|
|
990
|
+
`projects/${mr.projectId}/jobs/${job.id}/trace`
|
|
991
|
+
);
|
|
930
992
|
return {
|
|
931
993
|
provider: "gitlab",
|
|
932
994
|
project: mr.project,
|
|
@@ -959,7 +1021,12 @@ function hostFrom(cliHost, envHost, probe, fallback, labels) {
|
|
|
959
1021
|
if (cliHost) return cliHost;
|
|
960
1022
|
if (envHost) return envHost;
|
|
961
1023
|
const found = probe();
|
|
962
|
-
return resolveHost(
|
|
1024
|
+
return resolveHost(
|
|
1025
|
+
void 0,
|
|
1026
|
+
void 0,
|
|
1027
|
+
found.length > 0 ? found : fallback,
|
|
1028
|
+
labels
|
|
1029
|
+
);
|
|
963
1030
|
}
|
|
964
1031
|
function chooseKind(options = {}) {
|
|
965
1032
|
const env = options.env ?? process.env;
|
|
@@ -1012,7 +1079,13 @@ function connect(options = {}) {
|
|
|
1012
1079
|
const env = options.env ?? process.env;
|
|
1013
1080
|
const probe = options.probe ?? DEFAULT_PROBE;
|
|
1014
1081
|
if (chooseKind(options) === "github") {
|
|
1015
|
-
const host2 = hostFrom(
|
|
1082
|
+
const host2 = hostFrom(
|
|
1083
|
+
options.host,
|
|
1084
|
+
env.GITHUB_HOST,
|
|
1085
|
+
probe.github,
|
|
1086
|
+
[DOT_COM],
|
|
1087
|
+
GITHUB_LABELS
|
|
1088
|
+
);
|
|
1016
1089
|
const token2 = resolveToken(
|
|
1017
1090
|
host2,
|
|
1018
1091
|
options.token,
|
|
@@ -1022,7 +1095,13 @@ function connect(options = {}) {
|
|
|
1022
1095
|
);
|
|
1023
1096
|
return new GitHubProvider(host2, token2);
|
|
1024
1097
|
}
|
|
1025
|
-
const host = hostFrom(
|
|
1098
|
+
const host = hostFrom(
|
|
1099
|
+
options.host,
|
|
1100
|
+
env.GITLAB_HOST,
|
|
1101
|
+
probe.gitlab,
|
|
1102
|
+
[],
|
|
1103
|
+
GITLAB_LABELS
|
|
1104
|
+
);
|
|
1026
1105
|
const token = resolveToken(
|
|
1027
1106
|
host,
|
|
1028
1107
|
options.token,
|
|
@@ -1086,10 +1165,18 @@ function eventLine(event) {
|
|
|
1086
1165
|
}
|
|
1087
1166
|
function toMarkdown(report, lang = "en") {
|
|
1088
1167
|
const base = STRINGS[lang] ?? STRINGS.en;
|
|
1089
|
-
const t = {
|
|
1168
|
+
const t = {
|
|
1169
|
+
...base,
|
|
1170
|
+
...PROVIDER_STRINGS[report.provider][lang] ?? {}
|
|
1171
|
+
};
|
|
1090
1172
|
const ref = REF_PREFIX[report.provider];
|
|
1091
1173
|
const out = [];
|
|
1092
|
-
out.push(
|
|
1174
|
+
out.push(
|
|
1175
|
+
`# ${report.today.label} \u2014 ${report.user}`,
|
|
1176
|
+
"",
|
|
1177
|
+
`_${t.digest}_`,
|
|
1178
|
+
""
|
|
1179
|
+
);
|
|
1093
1180
|
if (report.previousDays.length === 0) {
|
|
1094
1181
|
out.push(`## ${t.previous}`, "", `_${t.nothing}_`, "");
|
|
1095
1182
|
}
|
|
@@ -1116,7 +1203,10 @@ function toMarkdown(report, lang = "en") {
|
|
|
1116
1203
|
}
|
|
1117
1204
|
const pending = report.reviews.filter((r) => !r.approvedByMe);
|
|
1118
1205
|
if (pending.length > 0) {
|
|
1119
|
-
out.push(
|
|
1206
|
+
out.push(
|
|
1207
|
+
`## ${t.reviews} (${report.reviewPendingCount} ${t.pending})`,
|
|
1208
|
+
""
|
|
1209
|
+
);
|
|
1120
1210
|
for (const review of pending) {
|
|
1121
1211
|
out.push(
|
|
1122
1212
|
`- \`${review.project}\` ${ref}${review.iid} ${review.title} \u2014 ${review.author}`
|
|
@@ -1127,7 +1217,9 @@ function toMarkdown(report, lang = "en") {
|
|
|
1127
1217
|
if (report.blockers.length > 0) {
|
|
1128
1218
|
out.push(`## ${t.blockers}`, "");
|
|
1129
1219
|
for (const blocker of report.blockers) {
|
|
1130
|
-
out.push(
|
|
1220
|
+
out.push(
|
|
1221
|
+
`- \`${blocker.project}\` ${ref}${blocker.mr} \u2014 job \`${blocker.job}\``
|
|
1222
|
+
);
|
|
1131
1223
|
out.push(...blocker.errors.map((line) => ` - \`${line}\``));
|
|
1132
1224
|
}
|
|
1133
1225
|
out.push("");
|
|
@@ -1195,21 +1287,29 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
1195
1287
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1196
1288
|
function readStandupSkillBody() {
|
|
1197
1289
|
const moduleDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1198
|
-
const skillPath = join2(
|
|
1290
|
+
const skillPath = join2(
|
|
1291
|
+
findPackageRoot(moduleDir),
|
|
1292
|
+
"skills",
|
|
1293
|
+
"standup",
|
|
1294
|
+
"SKILL.md"
|
|
1295
|
+
);
|
|
1199
1296
|
if (!existsSync2(skillPath)) {
|
|
1200
1297
|
throw new Error(`Cannot find the standup skill file at ${skillPath}`);
|
|
1201
1298
|
}
|
|
1202
1299
|
const content = readFileSync2(skillPath, "utf8");
|
|
1203
|
-
const withoutFrontmatter = content.replace(
|
|
1300
|
+
const withoutFrontmatter = content.replace(
|
|
1301
|
+
/^---\r?\n[\s\S]*?\r?\n---\r?\n/,
|
|
1302
|
+
""
|
|
1303
|
+
);
|
|
1204
1304
|
return withoutFrontmatter.replace(/^\n+/, "");
|
|
1205
1305
|
}
|
|
1206
1306
|
|
|
1207
1307
|
export {
|
|
1208
|
-
STALE_DAYS,
|
|
1209
1308
|
isoDay,
|
|
1210
1309
|
localAt,
|
|
1211
1310
|
label,
|
|
1212
1311
|
previousActiveDays,
|
|
1312
|
+
STALE_DAYS,
|
|
1213
1313
|
classify,
|
|
1214
1314
|
markMissingPipelines,
|
|
1215
1315
|
ConfigError,
|
|
@@ -1223,8 +1323,8 @@ export {
|
|
|
1223
1323
|
glabToken,
|
|
1224
1324
|
ghHosts,
|
|
1225
1325
|
ghToken,
|
|
1226
|
-
WEBHOOK_KINDS,
|
|
1227
1326
|
toChatText,
|
|
1327
|
+
WEBHOOK_KINDS,
|
|
1228
1328
|
postWebhook,
|
|
1229
1329
|
inferWebhookKind,
|
|
1230
1330
|
buildUrl,
|