github-reporadar 0.1.10 → 0.2.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 +86 -3
- package/dist-cli/cli.js +82 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,10 +101,20 @@ npx github-reporadar brief --org example-company --days 7 --private
|
|
|
101
101
|
### 一日中開いておく(受信箱)
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
npx github-reporadar --org example-company --watch
|
|
104
|
+
npx github-reporadar --org example-company --watch 1m
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
`--watch
|
|
107
|
+
`--watch`を付けると、開いたままその間隔でopen issue/PRを取り直します。同期のたびに前回との差分から「自分に起きたこと」を`events.json`(データdir)に足していきます。
|
|
108
|
+
|
|
109
|
+
毎回はopen issue/PRとレビュー状態だけを取り、コミットは30分ごとにまとめて取ります。GitHubのAPIは認証済みで5000回/時なので、実測にもとづく目安はこうです。
|
|
110
|
+
|
|
111
|
+
| 間隔 | 1時間の消費(完全同期を含む) | 上限に対して |
|
|
112
|
+
|---|---|---|
|
|
113
|
+
| 30s | 4,430 | 89%。`gh`コマンドを併用する余裕が少ない |
|
|
114
|
+
| 1m | 2,390 | 48%。おすすめ |
|
|
115
|
+
| 5m | 700 | 14% |
|
|
116
|
+
|
|
117
|
+
実測値は1回あたり、軽い同期がREST 34回で6秒、完全同期がREST 175回で13秒(42 repo、うち組織5)。repo数が増えると完全同期の側が伸びます。30秒未満は受け付けません。
|
|
108
118
|
|
|
109
119
|
| 出来事 | 条件 |
|
|
110
120
|
|---|---|
|
|
@@ -115,11 +125,84 @@ npx github-reporadar --org example-company --watch 5m
|
|
|
115
125
|
|
|
116
126
|
初回の同期では何も出しません。溜まっていた分を一斉に出すと、新しく起きたことが埋もれるためです。画面のバッジと通知は次の版で入ります。
|
|
117
127
|
|
|
128
|
+
### 平日の勤務時間だけ動かす(macOS)
|
|
129
|
+
|
|
130
|
+
launchdで平日11:00に起動し、19:00に停止します。土日は動きません。
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git clone https://github.com/BoxPistols/github-reporadar.git
|
|
134
|
+
cd github-reporadar
|
|
135
|
+
mkdir -p ~/.config/github-reporadar
|
|
136
|
+
cat > ~/.config/github-reporadar/workday.env <<'ENV'
|
|
137
|
+
RR_ORGS="example-company"
|
|
138
|
+
RR_WATCH=20m
|
|
139
|
+
RR_PORT=5177
|
|
140
|
+
ENV
|
|
141
|
+
./ops/install.sh
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
設定ファイルはリポジトリの外(`~/.config/github-reporadar/workday.env`)に置きます。組織名を含むので、リポジトリには入れません。
|
|
145
|
+
|
|
146
|
+
| コマンド | 中身 |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `./ops/install.sh` | launchdに登録。平日11:00起動 / 19:00停止 |
|
|
149
|
+
| `./ops/install.sh uninstall` | 登録を解除 |
|
|
150
|
+
| `./ops/reporadar-workday.sh start` / `stop` / `status` | 手で起動・停止・確認 |
|
|
151
|
+
|
|
152
|
+
ログは`~/.local/state/github-reporadar/workday.log`です。二重起動はしません。`gh`にログインしていなければ起動せず、理由をログに残します。
|
|
153
|
+
|
|
154
|
+
20分間隔なら、1時間あたりのGitHub APIの消費は約313回で上限の6%です(軽い同期が約1.5回、完全同期が約1.5回)。8時間動かしても上限には触れません。
|
|
155
|
+
|
|
156
|
+
launchdは対話シェルの`PATH`を継がないので、`install.sh`が実行時の`PATH`をplistに焼き込みます。`node`や`gh`の場所を変えたら登録し直してください。
|
|
157
|
+
|
|
158
|
+
### 毎日の運用
|
|
159
|
+
|
|
160
|
+
登録したあとは、こちらから何もしなくても動きます。
|
|
161
|
+
|
|
162
|
+
| 時刻 | 起きること |
|
|
163
|
+
|---|---|
|
|
164
|
+
| 平日11:00 | 起動して完全同期を1回。以後20分ごとにopen issue/PRを取り直す |
|
|
165
|
+
| 20分ごと | 受信箱に差分が溜まる。ブラウザを開いていれば通知が出る |
|
|
166
|
+
| 30分ごと | コミットも含めた完全同期 |
|
|
167
|
+
| 平日19:00 | 停止 |
|
|
168
|
+
| 土日 | 動かない |
|
|
169
|
+
|
|
170
|
+
自分でやることはこれだけです。
|
|
171
|
+
|
|
172
|
+
1. 朝、http://127.0.0.1:5177/ を開く。ブックマークしておく
|
|
173
|
+
2. フィルターバーの札で今日やることを決める(レビュー待ちの自分のPR、自分がレビューするPR、Draft、手を付けていないissue)
|
|
174
|
+
3. 受信箱に未読が付いたら開く。番号を押すとGitHubへ飛ぶ。読み終えたら「すべて既読」
|
|
175
|
+
4. 迷ったら壁打ちで「今日の作業順を決めたい」と聞く
|
|
176
|
+
|
|
177
|
+
初回だけ、受信箱の「通知を許可」を1回押してください。ブラウザを閉じている間の出来事も`events.json`に溜まるので、翌朝に開けば読めます。
|
|
178
|
+
|
|
179
|
+
### 様子がおかしいとき
|
|
180
|
+
|
|
181
|
+
| 症状 | 見るところ |
|
|
182
|
+
|---|---|
|
|
183
|
+
| 画面が開かない | `./ops/reporadar-workday.sh status`。動いていなければ`start` |
|
|
184
|
+
| 数字が古い | ログの末尾。`tail -20 ~/.local/state/github-reporadar/workday.log` |
|
|
185
|
+
| 受信箱が空のまま | 差分は前回の同期との比較で出る。初回同期の直後は必ず空。1回目の20分を待つ |
|
|
186
|
+
| 「gh にログインしていません」 | `gh auth login`。組織がSSOなら`gh auth refresh -h github.com -s read:org`も |
|
|
187
|
+
| 組織のrepoが出ない | 設定の`RR_ORGS`。`cat ~/.config/github-reporadar/workday.env` |
|
|
188
|
+
| 通知が来ない | ブラウザの通知許可。受信箱を開いて「通知を許可」が出ていないか |
|
|
189
|
+
| 起動しなくなった | `node`や`gh`の場所が変わった可能性。`./ops/install.sh`で登録し直す |
|
|
190
|
+
|
|
191
|
+
### 更新とやめかた
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
./ops/reporadar-workday.sh restart # 新しい版に入れ替える(npxが@latestを取り直す)
|
|
195
|
+
./ops/install.sh uninstall # 自動起動をやめる
|
|
196
|
+
./ops/reporadar-workday.sh stop # いま動いているものを止める
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
新しい版は次の11:00の起動から自動で使われます。すぐ入れ替えたいときだけ`restart`します。
|
|
200
|
+
|
|
118
201
|
### 運用の方針
|
|
119
202
|
|
|
120
203
|
| 方針 | 理由 |
|
|
121
204
|
|---|---|
|
|
122
|
-
| 「リアルタイム」はポーリング(`--watch`)で作る | GitHubのWebhook
|
|
205
|
+
| 「リアルタイム」はポーリング(`--watch`)で作る | GitHubのWebhookは受け口のサーバーが要り、ローカル完結と矛盾する。毎回の同期をopen issue/PRだけに絞ってREST 34回にしたので、1分間隔でも上限の半分で収まる(実測)。コミットは30分ごと |
|
|
123
206
|
| 出来事は前回との差分から作る | GitHubの通知APIは組織の全件が混ざり、自分のPRに関することだけを取り出しにくい。前回の`issues.json`と比べるだけなら追加のAPIも権限も要らない |
|
|
124
207
|
| 組織はローカルだけ | 上の「前提と境界」のとおり。公開版に足すなら組織管理者への説明と明示同意の画面が先 |
|
|
125
208
|
| 通知はブラウザのNotification API(次の版) | ページを開いている間に出る。OSの通知センターまで欲しい場合はCLI側から出す案があるが、まずはブラウザで足りるかを見る |
|
package/dist-cli/cli.js
CHANGED
|
@@ -366,6 +366,33 @@ async function listOpenIssues(token, repo, login) {
|
|
|
366
366
|
}
|
|
367
367
|
return items;
|
|
368
368
|
}
|
|
369
|
+
async function fetchIssuesOnly(token, fullNames, opts) {
|
|
370
|
+
const limit = createLimiter(CONCURRENCY);
|
|
371
|
+
const counts = await fetchOpenCounts(token, fullNames, limit);
|
|
372
|
+
const targets = fullNames.filter((n) => {
|
|
373
|
+
const c = counts.get(n);
|
|
374
|
+
return c != null && c.pr + c.issue > 0;
|
|
375
|
+
});
|
|
376
|
+
const lists = await Promise.all(
|
|
377
|
+
targets.map((n) => {
|
|
378
|
+
const [owner] = n.split("/");
|
|
379
|
+
const login = owner.toLowerCase() === opts.ownLogin.toLowerCase() ? null : opts.ownLogin;
|
|
380
|
+
return limit(() => listOpenIssues(token, { full_name: n }, login));
|
|
381
|
+
})
|
|
382
|
+
);
|
|
383
|
+
const flat = lists.flat();
|
|
384
|
+
const prMeta = await fetchPrMeta(
|
|
385
|
+
token,
|
|
386
|
+
flat.filter((it) => it.type === "pr").map((it) => ({ repo: it.repo, number: it.number })),
|
|
387
|
+
limit
|
|
388
|
+
);
|
|
389
|
+
const issues = flat.map((it) => {
|
|
390
|
+
if (it.type !== "pr") return it;
|
|
391
|
+
const meta = prMeta.get(`${it.repo}#${it.number}`);
|
|
392
|
+
return meta ? { ...it, ...meta } : it;
|
|
393
|
+
});
|
|
394
|
+
return { issues, counts };
|
|
395
|
+
}
|
|
369
396
|
function createProgress(repoTotal, issueTotal) {
|
|
370
397
|
const enabled = process.stdout.isTTY === true;
|
|
371
398
|
let repos = 0;
|
|
@@ -505,6 +532,16 @@ const DEFAULT_WINDOW_DAYS = 90;
|
|
|
505
532
|
async function runSync(opts) {
|
|
506
533
|
const windowDays = opts.days ?? DEFAULT_WINDOW_DAYS;
|
|
507
534
|
const log = opts.log ?? ((line) => console.log(line));
|
|
535
|
+
const readJson = (name) => {
|
|
536
|
+
const path = join(opts.dataDir, name);
|
|
537
|
+
if (!existsSync(path)) return null;
|
|
538
|
+
try {
|
|
539
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
if (opts.mode === "light") return runLightSync(opts, log, readJson);
|
|
508
545
|
if (!Number.isFinite(windowDays) || windowDays < 1 || windowDays > 365) {
|
|
509
546
|
throw new Error(`--days は 1〜365 で指定してください: ${windowDays}`);
|
|
510
547
|
}
|
|
@@ -516,15 +553,6 @@ async function runSync(opts) {
|
|
|
516
553
|
if (orgs.length > 0) log(`組織も取得: ${orgs.join(", ")}(自分がauthorのコミットだけ数える)`);
|
|
517
554
|
const { login, repos, activity, issues } = await fetchActivity(token, from, { includePrivate: true, orgs });
|
|
518
555
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
519
|
-
const readJson = (name) => {
|
|
520
|
-
const path = join(opts.dataDir, name);
|
|
521
|
-
if (!existsSync(path)) return null;
|
|
522
|
-
try {
|
|
523
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
524
|
-
} catch {
|
|
525
|
-
return null;
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
556
|
const prevIssues = readJson("issues.json");
|
|
529
557
|
const prevRepos = readJson("repos.json");
|
|
530
558
|
const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), {
|
|
@@ -543,6 +571,34 @@ async function runSync(opts) {
|
|
|
543
571
|
log(`OK: ${repos.length} repos / ${totalCommits} commits / ${issues.length} open issues+PRs → ${opts.dataDir}`);
|
|
544
572
|
return { login, repos: repos.length, commits: totalCommits, issues: issues.length, events: fresh.length };
|
|
545
573
|
}
|
|
574
|
+
async function runLightSync(opts, log, readJson) {
|
|
575
|
+
const prevRepos = readJson("repos.json");
|
|
576
|
+
if (!prevRepos || prevRepos.length === 0) {
|
|
577
|
+
log("前回の同期が無いので完全同期します");
|
|
578
|
+
return runSync({ ...opts, mode: "full" });
|
|
579
|
+
}
|
|
580
|
+
const token = getToken();
|
|
581
|
+
const login = readJson("meta.json")?.login;
|
|
582
|
+
if (!login) return runSync({ ...opts, mode: "full" });
|
|
583
|
+
const names = prevRepos.map((r) => r.nameWithOwner);
|
|
584
|
+
const { issues, counts } = await fetchIssuesOnly(token, names, { ownLogin: login });
|
|
585
|
+
const to = /* @__PURE__ */ new Date();
|
|
586
|
+
const prevIssues = readJson("issues.json");
|
|
587
|
+
const repoSet = new Set(names);
|
|
588
|
+
const fresh = diffEvents(prevIssues, issues, login, to.toISOString(), { prevRepos: repoSet, nextRepos: repoSet });
|
|
589
|
+
const events = mergeEvents(readJson("events.json") ?? [], fresh);
|
|
590
|
+
const repos = prevRepos.map((r) => {
|
|
591
|
+
const c = counts.get(r.nameWithOwner);
|
|
592
|
+
return c ? { ...r, openPrCount: c.pr, openIssueCount: c.issue } : r;
|
|
593
|
+
});
|
|
594
|
+
const meta = readJson("meta.json");
|
|
595
|
+
writeFileSync(join(opts.dataDir, "issues.json"), JSON.stringify(issues, null, 2));
|
|
596
|
+
writeFileSync(join(opts.dataDir, "events.json"), JSON.stringify(events, null, 2));
|
|
597
|
+
writeFileSync(join(opts.dataDir, "repos.json"), JSON.stringify(repos, null, 2));
|
|
598
|
+
if (meta) writeFileSync(join(opts.dataDir, "meta.json"), JSON.stringify({ ...meta, lastSyncAt: to.toISOString() }, null, 2));
|
|
599
|
+
log(`更新: open ${issues.length}件${fresh.length > 0 ? ` / 受信箱に${fresh.length}件` : ""}`);
|
|
600
|
+
return { login, repos: repos.length, commits: 0, issues: issues.length, events: fresh.length };
|
|
601
|
+
}
|
|
546
602
|
const isMain = process.argv[1]?.endsWith("sync.ts") || process.argv[1]?.endsWith("sync.js");
|
|
547
603
|
if (isMain) {
|
|
548
604
|
const daysArg = process.argv.indexOf("--days");
|
|
@@ -615,7 +671,7 @@ function parseArgs(argv) {
|
|
|
615
671
|
const m = v && !v.startsWith("--") ? /^(\d+)(s|m|h)$/.exec(v) : null;
|
|
616
672
|
if (!m) throw new RangeError(`--watchは 5m / 90s / 1h のように指定してください(受け取った値: ${v ?? "無し"})`);
|
|
617
673
|
const sec = Number(m[1]) * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1);
|
|
618
|
-
if (sec <
|
|
674
|
+
if (sec < 30) throw new RangeError(`--watchは30秒以上にしてください(GitHubのレート制限を守る)。受け取った値: ${v}`);
|
|
619
675
|
out.watchSeconds = sec;
|
|
620
676
|
break;
|
|
621
677
|
}
|
|
@@ -660,7 +716,7 @@ const USAGE = [
|
|
|
660
716
|
row("--port N", "待ち受けポート(既定5177。使用中なら次を探す)"),
|
|
661
717
|
row("--no-open", "ブラウザを開かない"),
|
|
662
718
|
row("--no-sync", "同期せずに手元のデータで開く"),
|
|
663
|
-
row("--watch
|
|
719
|
+
row("--watch 1m", "開いたまま、この間隔でopen issue/PRを取り直す(30秒以上)。受信箱に自分に起きたことが溜まる"),
|
|
664
720
|
row("--org NAME", "組織のrepoも取得する(繰り返し可)。組織では自分がauthorのコミットと、自分が作ったか担当のissue/PRだけ数える"),
|
|
665
721
|
row("--data-dir DIR", "データの置き場(既定: $XDG_CACHE_HOME か ~/.cache の github-reporadar/data)"),
|
|
666
722
|
row("--sort KEY", "brief の並び: active | recent | stale"),
|
|
@@ -819,12 +875,25 @@ async function main() {
|
|
|
819
875
|
if (args.open) openBrowser(url);
|
|
820
876
|
let timer = null;
|
|
821
877
|
if (args.watchSeconds) {
|
|
822
|
-
|
|
878
|
+
const FULL_EVERY_MS = 30 * 60 * 1e3;
|
|
879
|
+
console.log(`${args.watchSeconds}秒ごとにopen issue/PRを取り直します(コミットは30分ごと)`);
|
|
880
|
+
if (args.watchSeconds < 60) {
|
|
881
|
+
console.log("60秒未満はGitHubの制限に対する余裕が少なくなります(1時間あたり上限の約9割)");
|
|
882
|
+
}
|
|
823
883
|
let busy = false;
|
|
884
|
+
let lastFull = Date.now();
|
|
824
885
|
timer = setInterval(() => {
|
|
825
886
|
if (busy) return;
|
|
826
887
|
busy = true;
|
|
827
|
-
|
|
888
|
+
const full = Date.now() - lastFull >= FULL_EVERY_MS;
|
|
889
|
+
if (full) lastFull = Date.now();
|
|
890
|
+
runSync({
|
|
891
|
+
dataDir,
|
|
892
|
+
days: args.days,
|
|
893
|
+
orgs: args.orgs,
|
|
894
|
+
mode: full ? "full" : "light",
|
|
895
|
+
log: (line) => console.log(`[watch] ${line}`)
|
|
896
|
+
}).catch((err) => console.error(`[watch] 同期に失敗: ${err instanceof Error ? err.message : String(err)}`)).finally(() => {
|
|
828
897
|
busy = false;
|
|
829
898
|
});
|
|
830
899
|
}, args.watchSeconds * 1e3);
|