@toast-ninja/toast-cli 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/src/cli.js +57 -19
package/README.md CHANGED
@@ -49,11 +49,12 @@ toast review-ci wait \
49
49
  --repo toast-ninja/backend \
50
50
  --pr 1177 \
51
51
  --head "$GITHUB_SHA" \
52
- --timeout 20m \
53
- --interval 10s
52
+ --timeout 20m
54
53
  ```
55
54
 
56
55
  `wait` exits `0` only when Toast returns `acknowledged` for the exact head SHA. It exits `1` for actionable non-green states and `2` for auth, network, argument, or timeout errors.
56
+ It waits on Toast's server-side Review CI events through bounded requests, so a
57
+ local polling interval is not needed.
57
58
 
58
59
  CI can also provide a token directly:
59
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toast-ninja/toast-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Toast CLI for agent CI workflows",
5
5
  "bin": {
6
6
  "toast": "bin/toast.js"
package/src/cli.js CHANGED
@@ -11,7 +11,13 @@ const {
11
11
  const { requestJson } = require('./http')
12
12
 
13
13
  const DEFAULT_API_URL = 'https://api.toast.ninja'
14
- const WAIT_STATUSES = new Set(['pending', 'degraded:reconciliation_incomplete'])
14
+ const DEFAULT_WAIT_REQUEST_SECONDS = 25
15
+ const WAIT_REQUEST_GRACE_MS = 5000
16
+ const WAIT_STATUSES = new Set([
17
+ 'pending',
18
+ 'degraded:reconciliation_incomplete',
19
+ 'head_changed',
20
+ ])
15
21
  const REVIEW_CI_SKILL_PATH = path.join(
16
22
  __dirname,
17
23
  '..',
@@ -194,12 +200,25 @@ const openBrowserDefault = url =>
194
200
  })
195
201
  })
196
202
 
197
- const buildReviewStateUrl = ({ apiUrl, repo, pullNumber, head, endpoint }) =>
198
- `${apiUrl}/api/review-ci/repos/${encodeURIComponent(
203
+ const buildReviewStateUrl = ({
204
+ apiUrl,
205
+ repo,
206
+ pullNumber,
207
+ head,
208
+ endpoint,
209
+ query = {},
210
+ }) => {
211
+ const params = new URLSearchParams({
212
+ head,
213
+ ...query,
214
+ })
215
+
216
+ return `${apiUrl}/api/review-ci/repos/${encodeURIComponent(
199
217
  repo.owner,
200
218
  )}/${encodeURIComponent(repo.repo)}/pulls/${encodeURIComponent(
201
219
  pullNumber,
202
- )}/${endpoint}?head=${encodeURIComponent(head)}`
220
+ )}/${endpoint}?${params.toString()}`
221
+ }
203
222
 
204
223
  const requestReviewState = async ({
205
224
  options,
@@ -207,6 +226,8 @@ const requestReviewState = async ({
207
226
  config,
208
227
  request,
209
228
  endpoint = 'status',
229
+ waitTimeoutSeconds,
230
+ timeoutMs,
210
231
  }) => {
211
232
  const repo = parseRepo(requireOption(options, 'repo'))
212
233
  const pullNumber = requireOption(options, 'pr')
@@ -219,13 +240,26 @@ const requestReviewState = async ({
219
240
  'Missing Toast token. Run `toast review-ci auth` or set TOAST_TOKEN.',
220
241
  )
221
242
 
222
- const response = await request({
243
+ const requestOptions = {
223
244
  method: 'GET',
224
- url: buildReviewStateUrl({ apiUrl, repo, pullNumber, head, endpoint }),
245
+ url: buildReviewStateUrl({
246
+ apiUrl,
247
+ repo,
248
+ pullNumber,
249
+ head,
250
+ endpoint,
251
+ query: waitTimeoutSeconds
252
+ ? { wait_timeout_seconds: String(waitTimeoutSeconds) }
253
+ : {},
254
+ }),
225
255
  headers: {
226
256
  Authorization: `Bearer ${token}`,
227
257
  },
228
- })
258
+ }
259
+
260
+ if (timeoutMs) requestOptions.timeoutMs = timeoutMs
261
+
262
+ const response = await request(requestOptions)
229
263
 
230
264
  if (response.statusCode >= 400) {
231
265
  throw new Error(`Review CI request failed with HTTP ${response.statusCode}`)
@@ -297,13 +331,25 @@ const handleWait = async context => {
297
331
  const config = context.readConfig()
298
332
  const { options } = parseArgs(getReviewStateArgs(context))
299
333
  const timeoutMs = parseDurationMs(options.timeout || '20m')
300
- const intervalMs = parseDurationMs(options.interval || '10s')
334
+ if (options.interval) parseDurationMs(options.interval)
301
335
  const startedAt = toMillis(context.now())
302
- let elapsedMs = 0
303
336
 
304
- while (elapsedMs <= timeoutMs) {
337
+ while (toMillis(context.now()) - startedAt <= timeoutMs) {
338
+ const elapsedMs = toMillis(context.now()) - startedAt
339
+ const remainingMs = timeoutMs - elapsedMs
340
+ const waitTimeoutSeconds = Math.max(
341
+ 1,
342
+ Math.min(DEFAULT_WAIT_REQUEST_SECONDS, Math.ceil(remainingMs / 1000)),
343
+ )
305
344
  // eslint-disable-next-line no-await-in-loop
306
- const state = await requestReviewState({ ...context, config, options })
345
+ const state = await requestReviewState({
346
+ ...context,
347
+ config,
348
+ options,
349
+ endpoint: 'wait',
350
+ waitTimeoutSeconds,
351
+ timeoutMs: waitTimeoutSeconds * 1000 + WAIT_REQUEST_GRACE_MS,
352
+ })
307
353
 
308
354
  if (state.status === 'acknowledged') {
309
355
  writeReviewState({ state, stdout: context.stdout, asJson: options.json })
@@ -314,14 +360,6 @@ const handleWait = async context => {
314
360
  writeReviewState({ state, stdout: context.stdout, asJson: options.json })
315
361
  return 1
316
362
  }
317
-
318
- // eslint-disable-next-line no-await-in-loop
319
- await context.sleep(intervalMs)
320
- elapsedMs += intervalMs
321
-
322
- if (toMillis(context.now()) - startedAt > timeoutMs) {
323
- break
324
- }
325
363
  }
326
364
 
327
365
  context.stderr.write('Timed out waiting for Review CI acknowledgement\n')