@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21
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 +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- package/engine/jev/jev-client.ts +0 -173
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retries for `gh` calls (#569). A call that fails in a way likely to pass
|
|
3
|
+
* on a second try (a timeout, a cut-off JSON reply, a 5xx, a secondary rate
|
|
4
|
+
* limit, a dropped connection or a TLS error) is tried again, three tries in
|
|
5
|
+
* all, after waits of 2 s then 8 s. Any other failure throws at once. Each
|
|
6
|
+
* retry is logged to the run log.
|
|
7
|
+
*/
|
|
8
|
+
import { TrackerError } from './tracker'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
durationText,
|
|
12
|
+
runCommand,
|
|
13
|
+
type CommandResult,
|
|
14
|
+
} from '../shell/run-command'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs `gh` with `args` and collects its output. Never throws on a non-zero
|
|
18
|
+
* exit. The GitHub tracker makes every call through one, so tests can fake
|
|
19
|
+
* `gh`.
|
|
20
|
+
*/
|
|
21
|
+
export type GhRunner = (args: string[]) => Promise<
|
|
22
|
+
Pick<CommandResult, 'exit_code' | 'stdout' | 'stderr'> & {
|
|
23
|
+
timed_out?: boolean
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
/** A `gh` call that runs longer is stopped, and counts as transient. */
|
|
28
|
+
export const GH_TIMEOUT_MS = 60_000
|
|
29
|
+
|
|
30
|
+
/** The waits before the second and third tries. */
|
|
31
|
+
export const GH_RETRY_WAITS_MS = [2_000, 8_000]
|
|
32
|
+
|
|
33
|
+
/** How waits and retry lines happen; tests fake both. */
|
|
34
|
+
export type GhRetryDeps = {
|
|
35
|
+
sleep: (ms: number) => Promise<void>
|
|
36
|
+
log: (line: string) => void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const realRetryDeps: GhRetryDeps = {
|
|
40
|
+
sleep: (ms) => Bun.sleep(ms),
|
|
41
|
+
log: (line) => {
|
|
42
|
+
console.log(line)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The real `gh`, on the `PATH`, run in `cwd` (default: the engine's). */
|
|
47
|
+
export const realGh =
|
|
48
|
+
({ cwd }: { cwd?: string } = {}): GhRunner =>
|
|
49
|
+
(args) =>
|
|
50
|
+
runCommand({
|
|
51
|
+
cmd: ['gh', ...args],
|
|
52
|
+
cwd: cwd ?? process.cwd(),
|
|
53
|
+
timeout_ms: GH_TIMEOUT_MS,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
/** A failed `gh` call likely to pass on a second try. */
|
|
57
|
+
export class TransientGhError extends TrackerError {}
|
|
58
|
+
|
|
59
|
+
/** What `gh` says when a call fails in a way likely to pass next time. */
|
|
60
|
+
const TRANSIENT_STDERR = [
|
|
61
|
+
/unexpected end of JSON input/i,
|
|
62
|
+
/unexpected EOF/i,
|
|
63
|
+
/HTTP 5\d\d/,
|
|
64
|
+
/secondary rate limit/i,
|
|
65
|
+
/connection (reset|refused|timed out)/i,
|
|
66
|
+
/ECONNRESET|ECONNREFUSED|ETIMEDOUT/,
|
|
67
|
+
/i\/o timeout|deadline exceeded|timeout exceeded/i,
|
|
68
|
+
/\bTLS\b/i,
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
const NOT_FOUND = /HTTP 404\b/
|
|
72
|
+
|
|
73
|
+
/** A failed `gh` call as a `TrackerError`, in `gh`'s own words. */
|
|
74
|
+
const ghError = ({
|
|
75
|
+
args,
|
|
76
|
+
result,
|
|
77
|
+
}: {
|
|
78
|
+
args: string[]
|
|
79
|
+
result: Awaited<ReturnType<GhRunner>>
|
|
80
|
+
}): TrackerError => {
|
|
81
|
+
const what = `\`gh ${args.slice(0, 2).join(' ')}\``
|
|
82
|
+
if (result.timed_out === true) {
|
|
83
|
+
return new TransientGhError(
|
|
84
|
+
`${what} failed (timed out after ${durationText(GH_TIMEOUT_MS)})`
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
const message = `${what} failed (exit code ${result.exit_code}): ${result.stderr.trim()}`
|
|
88
|
+
return TRANSIENT_STDERR.some((pattern) => pattern.test(result.stderr))
|
|
89
|
+
? new TransientGhError(message)
|
|
90
|
+
: new TrackerError(message)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Runs `attempt` until it succeeds, it fails in a way that isn't transient,
|
|
95
|
+
* or the tries run out; then throws its last error. Before a retry,
|
|
96
|
+
* `landed` (for writes) looks for what the earlier try may have left: when
|
|
97
|
+
* it finds it, that is the answer and nothing is tried again.
|
|
98
|
+
*/
|
|
99
|
+
export const withRetries = async <T>({
|
|
100
|
+
attempt,
|
|
101
|
+
landed,
|
|
102
|
+
sleep,
|
|
103
|
+
log,
|
|
104
|
+
}: GhRetryDeps & {
|
|
105
|
+
attempt: () => Promise<T>
|
|
106
|
+
landed?: () => Promise<T | null>
|
|
107
|
+
}): Promise<T> => {
|
|
108
|
+
for (let tries = 1; ; tries += 1) {
|
|
109
|
+
try {
|
|
110
|
+
return await attempt()
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const wait = GH_RETRY_WAITS_MS[tries - 1]
|
|
113
|
+
if (!(error instanceof TransientGhError) || wait === undefined) {
|
|
114
|
+
throw error
|
|
115
|
+
}
|
|
116
|
+
log(
|
|
117
|
+
`[tracker] ${error.message}; trying again in ${durationText(wait)} (try ${tries + 1} of ${GH_RETRY_WAITS_MS.length + 1})`
|
|
118
|
+
)
|
|
119
|
+
await sleep(wait)
|
|
120
|
+
const earlier = landed === undefined ? null : await landed()
|
|
121
|
+
if (earlier !== null) return earlier
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The `gh` calls the tracker makes, each with retries:
|
|
128
|
+
* - `ok` returns the output, and throws when the call fails;
|
|
129
|
+
* - `read` parses the output with `parse`; output that doesn't parse as
|
|
130
|
+
* JSON counts as a cut-off reply, so as transient;
|
|
131
|
+
* - `api` reads `gh api <path>` as JSON, and is `null` only for a 404.
|
|
132
|
+
*/
|
|
133
|
+
export const createGhCalls = ({
|
|
134
|
+
run,
|
|
135
|
+
sleep,
|
|
136
|
+
log,
|
|
137
|
+
}: GhRetryDeps & { run: GhRunner }) => {
|
|
138
|
+
const once = async ({
|
|
139
|
+
args,
|
|
140
|
+
not_found_null,
|
|
141
|
+
}: {
|
|
142
|
+
args: string[]
|
|
143
|
+
not_found_null: boolean
|
|
144
|
+
}): Promise<string | null> => {
|
|
145
|
+
const result = await run(args)
|
|
146
|
+
if (result.exit_code === 0 && result.timed_out !== true) {
|
|
147
|
+
return result.stdout
|
|
148
|
+
}
|
|
149
|
+
if (not_found_null && NOT_FOUND.test(result.stderr)) return null
|
|
150
|
+
throw ghError({ args, result })
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const parsed = <T>({
|
|
154
|
+
args,
|
|
155
|
+
stdout,
|
|
156
|
+
parse,
|
|
157
|
+
}: {
|
|
158
|
+
args: string[]
|
|
159
|
+
stdout: string
|
|
160
|
+
parse: (stdout: string) => T
|
|
161
|
+
}): T => {
|
|
162
|
+
try {
|
|
163
|
+
return parse(stdout)
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (!(error instanceof SyntaxError)) throw error
|
|
166
|
+
throw new TransientGhError(
|
|
167
|
+
`\`gh ${args.slice(0, 2).join(' ')}\` failed (a cut-off reply: ${error.message})`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const read = <T>({
|
|
173
|
+
args,
|
|
174
|
+
parse,
|
|
175
|
+
landed,
|
|
176
|
+
}: {
|
|
177
|
+
args: string[]
|
|
178
|
+
parse: (stdout: string) => T
|
|
179
|
+
landed?: () => Promise<T | null>
|
|
180
|
+
}): Promise<T> =>
|
|
181
|
+
withRetries({
|
|
182
|
+
attempt: async () =>
|
|
183
|
+
parsed({
|
|
184
|
+
args,
|
|
185
|
+
stdout: (await once({ args, not_found_null: false })) ?? '',
|
|
186
|
+
parse,
|
|
187
|
+
}),
|
|
188
|
+
landed,
|
|
189
|
+
sleep,
|
|
190
|
+
log,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
read,
|
|
195
|
+
ok: ({
|
|
196
|
+
args,
|
|
197
|
+
landed,
|
|
198
|
+
}: {
|
|
199
|
+
args: string[]
|
|
200
|
+
landed?: () => Promise<string | null>
|
|
201
|
+
}): Promise<string> =>
|
|
202
|
+
read({ args, parse: (stdout) => stdout, landed }),
|
|
203
|
+
api: ({ path }: { path: string }): Promise<unknown> => {
|
|
204
|
+
const args = ['api', path]
|
|
205
|
+
return withRetries({
|
|
206
|
+
attempt: async () => {
|
|
207
|
+
const stdout = await once({ args, not_found_null: true })
|
|
208
|
+
return stdout === null
|
|
209
|
+
? null
|
|
210
|
+
: parsed({ args, stdout, parse: JSON.parse })
|
|
211
|
+
},
|
|
212
|
+
sleep,
|
|
213
|
+
log,
|
|
214
|
+
})
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { $ } from 'bun'
|
|
2
|
-
|
|
3
1
|
import { z } from 'zod'
|
|
4
2
|
|
|
5
3
|
import {
|
|
4
|
+
createGhCalls,
|
|
5
|
+
realGh,
|
|
6
|
+
realRetryDeps,
|
|
7
|
+
TransientGhError,
|
|
8
|
+
type GhRetryDeps,
|
|
9
|
+
type GhRunner,
|
|
10
|
+
} from './gh-retry'
|
|
11
|
+
import {
|
|
12
|
+
labelDefinition,
|
|
6
13
|
OpenedPullRequestSchema,
|
|
7
14
|
TrackerCommentSchema,
|
|
8
15
|
TrackerIssueSchema,
|
|
16
|
+
type LabelDefinition,
|
|
9
17
|
type Tracker,
|
|
10
18
|
type TrackerIssue,
|
|
11
19
|
} from './tracker'
|
|
12
20
|
|
|
21
|
+
export type { GhRunner } from './gh-retry'
|
|
22
|
+
|
|
13
23
|
/** An issue as GitHub's REST API returns it (only the fields we use). */
|
|
14
24
|
const GitHubIssueSchema = z.object({
|
|
15
25
|
number: z.number().int().positive(),
|
|
@@ -33,17 +43,18 @@ const GitHubIssueNumbersSchema = z.array(
|
|
|
33
43
|
z.object({ number: z.number().int().positive() })
|
|
34
44
|
)
|
|
35
45
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Pull requests as `gh pr list --json number,url,isCrossRepository` prints
|
|
48
|
+
* them. A fork's PR has `isCrossRepository` set.
|
|
49
|
+
*/
|
|
50
|
+
const GitHubPullListSchema = z.array(
|
|
51
|
+
OpenedPullRequestSchema.extend({ isCrossRepository: z.boolean() })
|
|
52
|
+
)
|
|
38
53
|
|
|
39
54
|
type GitHubIssue = z.infer<typeof GitHubIssueSchema>
|
|
40
55
|
|
|
41
|
-
/**
|
|
42
|
-
const
|
|
43
|
-
const result = await $`gh api ${path}`.quiet().nothrow()
|
|
44
|
-
if (result.exitCode !== 0) return null
|
|
45
|
-
return JSON.parse(result.stdout.toString())
|
|
46
|
-
}
|
|
56
|
+
/** Whether `gh label create` failed because the label is there already. */
|
|
57
|
+
const ALREADY_EXISTS = /already exists/i
|
|
47
58
|
|
|
48
59
|
const parseOrThrow = <T>({
|
|
49
60
|
schema,
|
|
@@ -65,14 +76,50 @@ const parseOrThrow = <T>({
|
|
|
65
76
|
|
|
66
77
|
/**
|
|
67
78
|
* The real tracker: GitHub issues through the `gh` CLI, which must be
|
|
68
|
-
* installed and logged in.
|
|
69
|
-
*
|
|
79
|
+
* installed and logged in. Engine tests use the in-memory tracker; the
|
|
80
|
+
* tracker's own tests pass a fake `gh`.
|
|
81
|
+
*
|
|
82
|
+
* Adding a label the repo lacks creates it first (#500): when the add
|
|
83
|
+
* fails, the repo's labels are read, and a missing one is created as
|
|
84
|
+
* `labelDefinition` describes it, then added again. A failure with the
|
|
85
|
+
* label there is thrown as it is. The happy path costs one call, and
|
|
86
|
+
* nothing hangs on the wording of `gh`'s error.
|
|
87
|
+
*
|
|
88
|
+
* Every call retries transient failures (see `gh-retry.ts`). A read throws
|
|
89
|
+
* after its last try; only a 404 reads as none. A write checks whether its
|
|
90
|
+
* first try landed before trying again: a comment with the same body is
|
|
91
|
+
* adopted, as is an open PR from the same branch, and a label that already
|
|
92
|
+
* exists counts as created.
|
|
93
|
+
*
|
|
94
|
+
* @param gh - Runs `gh`. Defaults to the real one.
|
|
95
|
+
* @param sleep - Waits between tries. Defaults to a real wait.
|
|
96
|
+
* @param log - Writes a retry to the run log. Defaults to stdout.
|
|
70
97
|
*
|
|
71
98
|
* @example
|
|
72
99
|
* const tracker = createGitHubTracker({ repo: 'asibilia/luca-framework' })
|
|
73
100
|
* const spec = await tracker.readSpec({ spec_number: 359 })
|
|
74
101
|
*/
|
|
75
|
-
export const createGitHubTracker = ({
|
|
102
|
+
export const createGitHubTracker = ({
|
|
103
|
+
repo,
|
|
104
|
+
gh,
|
|
105
|
+
sleep,
|
|
106
|
+
log,
|
|
107
|
+
}: {
|
|
108
|
+
repo: string
|
|
109
|
+
gh?: GhRunner
|
|
110
|
+
} & Partial<GhRetryDeps>): Tracker => {
|
|
111
|
+
const calls = createGhCalls({
|
|
112
|
+
run: gh ?? realGh(),
|
|
113
|
+
sleep: sleep ?? realRetryDeps.sleep,
|
|
114
|
+
log: log ?? realRetryDeps.log,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
/** Runs `gh` and returns its output; throws when it fails. */
|
|
118
|
+
const ghOk = (args: string[]): Promise<string> => calls.ok({ args })
|
|
119
|
+
|
|
120
|
+
/** Reads `gh api <path>` as JSON; `null` only for a 404. */
|
|
121
|
+
const ghApi = calls.api
|
|
122
|
+
|
|
76
123
|
const blockedBy = async (number: number): Promise<number[]> => {
|
|
77
124
|
const value = await ghApi({
|
|
78
125
|
path: `repos/${repo}/issues/${number}/dependencies/blocked_by`,
|
|
@@ -111,6 +158,133 @@ export const createGitHubTracker = ({ repo }: { repo: string }): Tracker => {
|
|
|
111
158
|
)
|
|
112
159
|
}
|
|
113
160
|
|
|
161
|
+
const listLabels: Tracker['listLabels'] = async () =>
|
|
162
|
+
(
|
|
163
|
+
await ghOk([
|
|
164
|
+
'label',
|
|
165
|
+
'list',
|
|
166
|
+
'--repo',
|
|
167
|
+
repo,
|
|
168
|
+
'--limit',
|
|
169
|
+
'1000',
|
|
170
|
+
'--json',
|
|
171
|
+
'name',
|
|
172
|
+
'--jq',
|
|
173
|
+
'.[].name',
|
|
174
|
+
])
|
|
175
|
+
)
|
|
176
|
+
.split('\n')
|
|
177
|
+
.filter((name) => name !== '')
|
|
178
|
+
|
|
179
|
+
const createLabelArgs = ({
|
|
180
|
+
name,
|
|
181
|
+
color,
|
|
182
|
+
description,
|
|
183
|
+
}: LabelDefinition): string[] => [
|
|
184
|
+
'label',
|
|
185
|
+
'create',
|
|
186
|
+
name,
|
|
187
|
+
'--repo',
|
|
188
|
+
repo,
|
|
189
|
+
'--color',
|
|
190
|
+
color,
|
|
191
|
+
'--description',
|
|
192
|
+
description,
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
const listComments: Tracker['listComments'] = async ({
|
|
196
|
+
number,
|
|
197
|
+
since_id,
|
|
198
|
+
}) =>
|
|
199
|
+
// One JSON object per line, across every page.
|
|
200
|
+
(
|
|
201
|
+
await calls.read({
|
|
202
|
+
args: [
|
|
203
|
+
'api',
|
|
204
|
+
'--paginate',
|
|
205
|
+
`repos/${repo}/issues/${number}/comments?per_page=100`,
|
|
206
|
+
'--jq',
|
|
207
|
+
'.[] | {id, author: (.user.login // ""), body: (.body // "")}',
|
|
208
|
+
],
|
|
209
|
+
parse: (listed) =>
|
|
210
|
+
listed
|
|
211
|
+
.split('\n')
|
|
212
|
+
.filter((line) => line.trim() !== '')
|
|
213
|
+
.map((line) =>
|
|
214
|
+
parseOrThrow({
|
|
215
|
+
schema: TrackerCommentSchema,
|
|
216
|
+
value: JSON.parse(line),
|
|
217
|
+
what: `a comment on #${number}`,
|
|
218
|
+
})
|
|
219
|
+
),
|
|
220
|
+
})
|
|
221
|
+
).filter(({ id }) => id > since_id)
|
|
222
|
+
|
|
223
|
+
/** Creates a label; one already there counts as created. */
|
|
224
|
+
const createLabel: Tracker['createLabel'] = async (label) => {
|
|
225
|
+
try {
|
|
226
|
+
await ghOk(createLabelArgs(label))
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (error instanceof Error && ALREADY_EXISTS.test(error.message)) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
throw error
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Adds or removes one label of an issue; never throws. */
|
|
236
|
+
const editLabel = ({
|
|
237
|
+
number,
|
|
238
|
+
label,
|
|
239
|
+
change,
|
|
240
|
+
}: {
|
|
241
|
+
number: number
|
|
242
|
+
label: string
|
|
243
|
+
change: 'add' | 'remove'
|
|
244
|
+
}): Promise<Error | null> =>
|
|
245
|
+
ghOk([
|
|
246
|
+
'issue',
|
|
247
|
+
'edit',
|
|
248
|
+
String(number),
|
|
249
|
+
'--repo',
|
|
250
|
+
repo,
|
|
251
|
+
`--${change}-label`,
|
|
252
|
+
label,
|
|
253
|
+
]).then(
|
|
254
|
+
() => null,
|
|
255
|
+
(error: Error) => error
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const findOpenPullRequest: Tracker['findOpenPullRequest'] = async ({
|
|
259
|
+
head,
|
|
260
|
+
}) => {
|
|
261
|
+
// `--head` matches the branch name alone, so a fork's PR from a
|
|
262
|
+
// branch of the same name is left out: it isn't the run's.
|
|
263
|
+
const pull = await calls.read({
|
|
264
|
+
args: [
|
|
265
|
+
'pr',
|
|
266
|
+
'list',
|
|
267
|
+
'--repo',
|
|
268
|
+
repo,
|
|
269
|
+
'--head',
|
|
270
|
+
head,
|
|
271
|
+
'--state',
|
|
272
|
+
'open',
|
|
273
|
+
'--json',
|
|
274
|
+
'number,url,isCrossRepository',
|
|
275
|
+
],
|
|
276
|
+
parse: (listed) =>
|
|
277
|
+
parseOrThrow({
|
|
278
|
+
schema: GitHubPullListSchema,
|
|
279
|
+
value: JSON.parse(listed),
|
|
280
|
+
what: `the open pull requests from ${head}`,
|
|
281
|
+
}).find(({ isCrossRepository }) => !isCrossRepository),
|
|
282
|
+
})
|
|
283
|
+
return pull === undefined
|
|
284
|
+
? null
|
|
285
|
+
: { number: pull.number, url: pull.url }
|
|
286
|
+
}
|
|
287
|
+
|
|
114
288
|
return {
|
|
115
289
|
readIssue,
|
|
116
290
|
readSpec: async ({ spec_number }) => {
|
|
@@ -132,79 +306,100 @@ export const createGitHubTracker = ({ repo }: { repo: string }): Tracker => {
|
|
|
132
306
|
})
|
|
133
307
|
return Promise.all(issues.map(toTrackerIssue))
|
|
134
308
|
},
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const listed =
|
|
147
|
-
await $`gh api --paginate repos/${repo}/issues/${number}/comments?per_page=100 --jq ${'.[] | {id, author: (.user.login // ""), body: (.body // "")}'}`.quiet()
|
|
148
|
-
return listed.stdout
|
|
149
|
-
.toString()
|
|
150
|
-
.split('\n')
|
|
151
|
-
.filter((line) => line.trim() !== '')
|
|
152
|
-
.map((line) =>
|
|
309
|
+
// Before a retry, a comment with the same body (its marker is in
|
|
310
|
+
// it) is the first try landing: it is adopted, not posted again.
|
|
311
|
+
comment: ({ number, body }) =>
|
|
312
|
+
calls.read({
|
|
313
|
+
args: [
|
|
314
|
+
'api',
|
|
315
|
+
`repos/${repo}/issues/${number}/comments`,
|
|
316
|
+
'-f',
|
|
317
|
+
`body=${body}`,
|
|
318
|
+
],
|
|
319
|
+
parse: (posted) =>
|
|
153
320
|
parseOrThrow({
|
|
154
|
-
schema:
|
|
155
|
-
value: JSON.parse(
|
|
156
|
-
what: `
|
|
157
|
-
})
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
|
|
321
|
+
schema: GitHubCommentPostedSchema,
|
|
322
|
+
value: JSON.parse(posted),
|
|
323
|
+
what: `the comment posted on #${number}`,
|
|
324
|
+
}),
|
|
325
|
+
landed: async () => {
|
|
326
|
+
const comments = await listComments({ number, since_id: 0 })
|
|
327
|
+
const same = comments.find((each) => each.body === body)
|
|
328
|
+
return same === undefined ? null : { id: same.id }
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
listComments,
|
|
161
332
|
addLabel: async ({ number, label }) => {
|
|
162
|
-
|
|
333
|
+
const failed = await editLabel({ number, label, change: 'add' })
|
|
334
|
+
if (failed === null) return
|
|
335
|
+
if ((await listLabels()).includes(label)) throw failed
|
|
336
|
+
// A failed create still tries the add again: another run may
|
|
337
|
+
// have just created the label.
|
|
338
|
+
const created = await createLabel(
|
|
339
|
+
labelDefinition({ name: label })
|
|
340
|
+
).then(
|
|
341
|
+
() => null,
|
|
342
|
+
(error: Error) => error
|
|
343
|
+
)
|
|
344
|
+
const again = await editLabel({ number, label, change: 'add' })
|
|
345
|
+
if (again === null) return
|
|
346
|
+
if (created === null) throw again
|
|
347
|
+
throw new Error(
|
|
348
|
+
`${again.message}; creating the label failed too: ${created.message}`
|
|
349
|
+
)
|
|
163
350
|
},
|
|
164
351
|
removeLabel: async ({ number, label }) => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
352
|
+
const failed = await editLabel({ number, label, change: 'remove' })
|
|
353
|
+
if (failed === null) return
|
|
354
|
+
// A label the repo lacks can't be on the issue.
|
|
355
|
+
if ((await listLabels()).includes(label)) throw failed
|
|
168
356
|
},
|
|
357
|
+
// A read that fails fails the close too, rather than closing blind.
|
|
169
358
|
closeIssue: async ({ number }) => {
|
|
170
359
|
const issue = await readIssue({ number })
|
|
171
360
|
if (issue?.state === 'closed') return
|
|
172
|
-
await
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return parseOrThrow({
|
|
182
|
-
schema: OpenedPullRequestSchema,
|
|
183
|
-
value: { number, url },
|
|
184
|
-
what: `the pull request from ${head}`,
|
|
185
|
-
})
|
|
186
|
-
},
|
|
187
|
-
findOpenPullRequest: async ({ head }) => {
|
|
188
|
-
const listed =
|
|
189
|
-
await $`gh pr list --repo ${repo} --head ${head} --state open --json number,url`.quiet()
|
|
190
|
-
const [pull] = parseOrThrow({
|
|
191
|
-
schema: GitHubPullListSchema,
|
|
192
|
-
value: JSON.parse(listed.stdout.toString()),
|
|
193
|
-
what: `the open pull requests from ${head}`,
|
|
194
|
-
})
|
|
195
|
-
return pull ?? null
|
|
196
|
-
},
|
|
197
|
-
listLabels: async () => {
|
|
198
|
-
const listed =
|
|
199
|
-
await $`gh label list --repo ${repo} --limit 1000 --json name --jq ${'.[].name'}`.quiet()
|
|
200
|
-
return listed.stdout
|
|
201
|
-
.toString()
|
|
202
|
-
.split('\n')
|
|
203
|
-
.filter((name) => name !== '')
|
|
204
|
-
},
|
|
205
|
-
createLabel: async ({ name, color, description }) => {
|
|
206
|
-
await $`gh label create ${name} --repo ${repo} --color ${color} --description ${description}`.quiet()
|
|
361
|
+
await ghOk([
|
|
362
|
+
'issue',
|
|
363
|
+
'close',
|
|
364
|
+
String(number),
|
|
365
|
+
'--repo',
|
|
366
|
+
repo,
|
|
367
|
+
'--reason',
|
|
368
|
+
'completed',
|
|
369
|
+
])
|
|
207
370
|
},
|
|
371
|
+
// Retried only once no open PR from `head` shows the first try
|
|
372
|
+
// landed; one that does is adopted.
|
|
373
|
+
openPullRequest: ({ head, base, title, body }) =>
|
|
374
|
+
calls.read({
|
|
375
|
+
args: [
|
|
376
|
+
'pr',
|
|
377
|
+
'create',
|
|
378
|
+
'--repo',
|
|
379
|
+
repo,
|
|
380
|
+
'--head',
|
|
381
|
+
head,
|
|
382
|
+
'--base',
|
|
383
|
+
base,
|
|
384
|
+
'--title',
|
|
385
|
+
title,
|
|
386
|
+
'--body',
|
|
387
|
+
body,
|
|
388
|
+
],
|
|
389
|
+
parse: (created) => {
|
|
390
|
+
const url = created.trim()
|
|
391
|
+
const number = Number(/\/pull\/(\d+)/.exec(url)?.[1])
|
|
392
|
+
return parseOrThrow({
|
|
393
|
+
schema: OpenedPullRequestSchema,
|
|
394
|
+
value: { number, url },
|
|
395
|
+
what: `the pull request from ${head}`,
|
|
396
|
+
})
|
|
397
|
+
},
|
|
398
|
+
landed: () => findOpenPullRequest({ head }),
|
|
399
|
+
}),
|
|
400
|
+
findOpenPullRequest,
|
|
401
|
+
listLabels,
|
|
402
|
+
createLabel,
|
|
208
403
|
// Checked on the repo's newest issue; a repo with no issues yet
|
|
209
404
|
// can't show them missing, so both count as there.
|
|
210
405
|
issueLinks: async () => {
|
|
@@ -245,23 +440,48 @@ export const githubRepoOf = async ({
|
|
|
245
440
|
}: {
|
|
246
441
|
repo: string
|
|
247
442
|
}): Promise<string> => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
443
|
+
const calls = createGhCalls({
|
|
444
|
+
run: realGh({ cwd: repo }),
|
|
445
|
+
...realRetryDeps,
|
|
446
|
+
})
|
|
447
|
+
const name = await calls
|
|
448
|
+
.ok({
|
|
449
|
+
args: [
|
|
450
|
+
'repo',
|
|
451
|
+
'view',
|
|
452
|
+
'--json',
|
|
453
|
+
'nameWithOwner',
|
|
454
|
+
'--jq',
|
|
455
|
+
'.nameWithOwner',
|
|
456
|
+
],
|
|
457
|
+
})
|
|
458
|
+
.then(
|
|
459
|
+
(stdout) => stdout.trim(),
|
|
460
|
+
(error: Error) => {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`Could not find the GitHub repo of ${repo} with gh: ${error.message}`
|
|
463
|
+
)
|
|
464
|
+
}
|
|
257
465
|
)
|
|
466
|
+
if (name === '') {
|
|
467
|
+
throw new Error(`Could not find the GitHub repo of ${repo} with gh`)
|
|
258
468
|
}
|
|
259
469
|
return name
|
|
260
470
|
}
|
|
261
471
|
|
|
262
|
-
/**
|
|
472
|
+
/**
|
|
473
|
+
* The login `gh` is signed in as, or `null` when it isn't. A transient
|
|
474
|
+
* failure is retried, and throws after the last try.
|
|
475
|
+
*/
|
|
263
476
|
export const ghLogin = async (): Promise<string | null> => {
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
477
|
+
const calls = createGhCalls({ run: realGh(), ...realRetryDeps })
|
|
478
|
+
try {
|
|
479
|
+
const login = (
|
|
480
|
+
await calls.ok({ args: ['api', 'user', '--jq', '.login'] })
|
|
481
|
+
).trim()
|
|
482
|
+
return login === '' ? null : login
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (error instanceof TransientGhError) throw error
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
267
487
|
}
|