@quenty/nevermore-cli 4.43.0-canary.783.30683872723.0 → 4.43.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 +13 -16
- package/README.md +10 -0
- package/dist/commands/batch-command/batch-deploy-command.d.ts.map +1 -1
- package/dist/commands/batch-command/batch-deploy-command.js +101 -37
- package/dist/commands/batch-command/batch-deploy-command.js.map +1 -1
- package/dist/commands/deploy-command/index.d.ts +2 -0
- package/dist/commands/deploy-command/index.d.ts.map +1 -1
- package/dist/commands/deploy-command/index.js +35 -22
- package/dist/commands/deploy-command/index.js.map +1 -1
- package/dist/utils/job-context/base-job-context.d.ts.map +1 -1
- package/dist/utils/job-context/base-job-context.js +1 -2
- package/dist/utils/job-context/base-job-context.js.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.d.ts +8 -4
- package/dist/utils/open-cloud/open-cloud-client.d.ts.map +1 -1
- package/dist/utils/open-cloud/open-cloud-client.js +8 -4
- package/dist/utils/open-cloud/open-cloud-client.js.map +1 -1
- package/dist/utils/watch/github-context.test.js +9 -1
- package/dist/utils/watch/github-context.test.js.map +1 -1
- package/dist/utils/watch/http-watch-registry.js +5 -3
- package/dist/utils/watch/http-watch-registry.js.map +1 -1
- package/dist/utils/watch/stream-watch-loop.d.ts.map +1 -1
- package/dist/utils/watch/stream-watch-loop.js +42 -51
- package/dist/utils/watch/stream-watch-loop.js.map +1 -1
- package/package.json +5 -5
- package/src/commands/batch-command/batch-deploy-command.ts +135 -41
- package/src/commands/deploy-command/index.ts +41 -24
- package/src/utils/job-context/base-job-context.ts +1 -2
- package/src/utils/open-cloud/open-cloud-client.ts +8 -4
- package/src/utils/watch/github-context.test.ts +10 -1
- package/src/utils/watch/http-watch-registry.ts +5 -3
- package/src/utils/watch/stream-watch-loop.ts +73 -59
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -43,8 +43,16 @@ import {
|
|
|
43
43
|
readPackageVersionAsync,
|
|
44
44
|
} from '../../utils/nevermore-cli-utils.js';
|
|
45
45
|
import { parseTestLogs } from '../../utils/testing/test-log-parser.js';
|
|
46
|
-
import {
|
|
47
|
-
|
|
46
|
+
import {
|
|
47
|
+
buildWatchMonitorName,
|
|
48
|
+
parseWatchOption,
|
|
49
|
+
type BasePlaceResolver,
|
|
50
|
+
type WatchOption,
|
|
51
|
+
} from '@quenty/nevermore-deploy';
|
|
52
|
+
import {
|
|
53
|
+
registerWatchesAsync,
|
|
54
|
+
type WatchCandidate,
|
|
55
|
+
} from '../../utils/watch/register-watches.js';
|
|
48
56
|
|
|
49
57
|
const SMOKE_TEST_SCRIPT_PATH = resolvePackagePath(
|
|
50
58
|
import.meta.url,
|
|
@@ -185,6 +193,27 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
185
193
|
'Use --all to deploy every package with this target, or --base <ref> to change the comparison ref.'
|
|
186
194
|
);
|
|
187
195
|
}
|
|
196
|
+
// Renew anyway. Registration derives from committed config plus the lock,
|
|
197
|
+
// so it does not need this run to have deployed anything — and the lease is
|
|
198
|
+
// the only thing keeping the watch alive. Returning here instead is how a
|
|
199
|
+
// repo whose scheduled builds all diff clean quietly stops being watched,
|
|
200
|
+
// with no failure anywhere to notice.
|
|
201
|
+
if (watchOption) {
|
|
202
|
+
OutputHelper.info(
|
|
203
|
+
'Renewing watches anyway, so the lease does not lapse.'
|
|
204
|
+
);
|
|
205
|
+
await _registerWatchesAsync({
|
|
206
|
+
targetName,
|
|
207
|
+
option: watchOption,
|
|
208
|
+
// Nothing deployed, so the lock is the whole truth about what each
|
|
209
|
+
// place was built from.
|
|
210
|
+
resolver: createLockOnlyBasePlaceResolver(),
|
|
211
|
+
useGhAuth: args.watchUseGhAuth,
|
|
212
|
+
shareApiKey: args.watchShareApiKey,
|
|
213
|
+
resolveApiKeyAsync: () => getApiKeyAsync(args),
|
|
214
|
+
dryrun: args.dryrun,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
188
217
|
return;
|
|
189
218
|
}
|
|
190
219
|
|
|
@@ -197,20 +226,7 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
197
226
|
// lock file, so it runs for real even on a dryrun — that is what makes the
|
|
198
227
|
// watch path testable without shipping a build.
|
|
199
228
|
if (watchOption) {
|
|
200
|
-
|
|
201
|
-
'[DRYRUN] Registering separate dryrun watches and firing them, to ' +
|
|
202
|
-
'prove routing works. This does not build or upload here — but ' +
|
|
203
|
-
'firing really does dispatch the workflow once per package, and ' +
|
|
204
|
-
'each of those runs a real deploy on the runner.'
|
|
205
|
-
);
|
|
206
|
-
if (args.watchShareApiKey) {
|
|
207
|
-
OutputHelper.warn(
|
|
208
|
-
'[DRYRUN] Registering without the Open Cloud key, so a "saved" base ' +
|
|
209
|
-
'place is skipped here even though a real run would watch it.'
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
const allPackages = await discoverAllTargetPackagesAsync(targetName);
|
|
213
|
-
// Under its own monitor name, never the one a real run owns.
|
|
229
|
+
// Under its own monitor names, never the ones a real run owns.
|
|
214
230
|
//
|
|
215
231
|
// Re-registering replaces a monitor's entire watch list, and a dryrun
|
|
216
232
|
// cannot reproduce a real registration: the Open Cloud key is resolved
|
|
@@ -219,18 +235,13 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
219
235
|
// driver and its content-hash vocabulary, dropping the baselines, and
|
|
220
236
|
// deleting any "saved" watch outright, because an anonymous
|
|
221
237
|
// registration skips those.
|
|
222
|
-
await
|
|
238
|
+
await _registerWatchesAsync({
|
|
239
|
+
targetName,
|
|
223
240
|
option: watchOption,
|
|
224
|
-
monitorName: `${targetName}/dryrun`,
|
|
225
241
|
resolver: createLockOnlyBasePlaceResolver(),
|
|
226
242
|
useGhAuth: args.watchUseGhAuth,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
targetName,
|
|
230
|
-
packageName: buildTarget.packageName,
|
|
231
|
-
packagePath: buildTarget.path,
|
|
232
|
-
place: buildTarget.target,
|
|
233
|
-
})),
|
|
243
|
+
shareApiKey: args.watchShareApiKey,
|
|
244
|
+
dryrun: true,
|
|
234
245
|
});
|
|
235
246
|
}
|
|
236
247
|
return;
|
|
@@ -394,25 +405,13 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
394
405
|
// re-registers the same wrong baseline. The failure becomes permanent and
|
|
395
406
|
// invisible.
|
|
396
407
|
if (watchOption && results.summary.failed === 0) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
// re-apply replaces its whole list, so registering only what changed
|
|
400
|
-
// would delete the watches of every package that happened not to change
|
|
401
|
-
// this run. Packages that did not deploy still have a lock entry, which
|
|
402
|
-
// is the correct baseline for them.
|
|
403
|
-
const allPackages = await discoverAllTargetPackagesAsync(targetName);
|
|
404
|
-
await registerWatchesAsync({
|
|
408
|
+
await _registerWatchesAsync({
|
|
409
|
+
targetName,
|
|
405
410
|
option: watchOption,
|
|
406
|
-
monitorName: targetName,
|
|
407
411
|
resolver: basePlaceResolver,
|
|
408
412
|
useGhAuth: args.watchUseGhAuth,
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
targetName,
|
|
412
|
-
packageName: buildTarget.packageName,
|
|
413
|
-
packagePath: buildTarget.path,
|
|
414
|
-
place: buildTarget.target,
|
|
415
|
-
})),
|
|
413
|
+
shareApiKey: args.watchShareApiKey,
|
|
414
|
+
resolveApiKeyAsync: async () => apiKey,
|
|
416
415
|
});
|
|
417
416
|
}
|
|
418
417
|
} catch (err) {
|
|
@@ -426,6 +425,101 @@ async function _runAsync(args: BatchDeployArgs): Promise<void> {
|
|
|
426
425
|
process.exit(exitCode);
|
|
427
426
|
}
|
|
428
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Register one monitor per package, covering every package with this target.
|
|
430
|
+
*
|
|
431
|
+
* Per package rather than one for the batch, because `deploy run --watch`
|
|
432
|
+
* registers a package's places too. Two names for one package's watches means
|
|
433
|
+
* two monitors on the same base place, so one artist publish dispatches twice
|
|
434
|
+
* and each rebuild races the other's lock write. Both paths take the name from
|
|
435
|
+
* `buildWatchMonitorName`, so registering from either replaces the same list.
|
|
436
|
+
*
|
|
437
|
+
* Every package with the target, never just the ones that deployed: a re-apply
|
|
438
|
+
* replaces a monitor's whole list, and a package that didn't change still has a
|
|
439
|
+
* lock entry, which is exactly the right baseline for it.
|
|
440
|
+
*/
|
|
441
|
+
async function _registerWatchesAsync(options: {
|
|
442
|
+
targetName: string;
|
|
443
|
+
option: WatchOption;
|
|
444
|
+
resolver: BasePlaceResolver;
|
|
445
|
+
useGhAuth?: boolean;
|
|
446
|
+
/** Whether `--watch-share-api-key` was passed. A dryrun never shares one. */
|
|
447
|
+
shareApiKey?: boolean;
|
|
448
|
+
/** Called only if a key is actually going to be sent. */
|
|
449
|
+
resolveApiKeyAsync?: () => Promise<string>;
|
|
450
|
+
dryrun?: boolean;
|
|
451
|
+
}): Promise<void> {
|
|
452
|
+
const { targetName } = options;
|
|
453
|
+
|
|
454
|
+
if (options.dryrun) {
|
|
455
|
+
OutputHelper.info(
|
|
456
|
+
'[DRYRUN] Registering separate dryrun watches and firing them, to ' +
|
|
457
|
+
'prove routing works. This does not build or upload here — but ' +
|
|
458
|
+
'firing really does dispatch the workflow once per package, and ' +
|
|
459
|
+
'each of those runs a real deploy on the runner.'
|
|
460
|
+
);
|
|
461
|
+
if (options.shareApiKey) {
|
|
462
|
+
OutputHelper.warn(
|
|
463
|
+
'[DRYRUN] Registering without the Open Cloud key, so a "saved" base ' +
|
|
464
|
+
'place is skipped here even though a real run would watch it.'
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const robloxApiKey =
|
|
470
|
+
options.shareApiKey && !options.dryrun
|
|
471
|
+
? await options.resolveApiKeyAsync?.()
|
|
472
|
+
: undefined;
|
|
473
|
+
|
|
474
|
+
const allPackages = await discoverAllTargetPackagesAsync(targetName);
|
|
475
|
+
|
|
476
|
+
// Places that never asked to be watched are dropped here rather than passed
|
|
477
|
+
// in to be reported as skips. Most places in a batch are in that state, and
|
|
478
|
+
// per-package registration would otherwise warn about each package that has
|
|
479
|
+
// no watch at all. A place that asked and can't be watched still goes
|
|
480
|
+
// through, because that is the case worth saying out loud.
|
|
481
|
+
const byPackage = new Map<string, WatchCandidate[]>();
|
|
482
|
+
for (const buildTarget of flattenToBatchTargets(allPackages)) {
|
|
483
|
+
if (buildTarget.target.watch == null) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const candidates = byPackage.get(buildTarget.packageName) ?? [];
|
|
487
|
+
candidates.push({
|
|
488
|
+
targetName,
|
|
489
|
+
packageName: buildTarget.packageName,
|
|
490
|
+
packagePath: buildTarget.path,
|
|
491
|
+
place: buildTarget.target,
|
|
492
|
+
});
|
|
493
|
+
byPackage.set(buildTarget.packageName, candidates);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (byPackage.size === 0) {
|
|
497
|
+
OutputHelper.warn(
|
|
498
|
+
`--watch was requested, but no place in a "${targetName}" target has a ` +
|
|
499
|
+
'"watch" field. Add one naming the workflow that rebuilds it.'
|
|
500
|
+
);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
for (const [packageName, candidates] of byPackage) {
|
|
505
|
+
await registerWatchesAsync({
|
|
506
|
+
option: options.option,
|
|
507
|
+
monitorName: buildWatchMonitorName({
|
|
508
|
+
packageName,
|
|
509
|
+
targetName,
|
|
510
|
+
dryrun: options.dryrun,
|
|
511
|
+
}),
|
|
512
|
+
resolver: options.resolver,
|
|
513
|
+
useGhAuth: options.useGhAuth,
|
|
514
|
+
robloxApiKey,
|
|
515
|
+
// Firing proves the workflow really receives the selector, which is the
|
|
516
|
+
// half that historically breaks. Only a dryrun asks for it.
|
|
517
|
+
triggerAfterRegister: options.dryrun,
|
|
518
|
+
candidates,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
429
523
|
function _annotateSmokeTestFailure(logs: string): string {
|
|
430
524
|
const header =
|
|
431
525
|
'Post-deploy smoke test failed. The deploy itself succeeded, but a server ' +
|
|
@@ -39,16 +39,20 @@ import {
|
|
|
39
39
|
readPackageVersionAsync,
|
|
40
40
|
} from '../../utils/nevermore-cli-utils.js';
|
|
41
41
|
import {
|
|
42
|
+
WATCH_MODES,
|
|
43
|
+
buildWatchMonitorName,
|
|
44
|
+
checkBasePlaceWatchable,
|
|
42
45
|
formatTargetSelector,
|
|
43
|
-
isBasePlaceVersionKeyword,
|
|
44
46
|
loadDeployConfigAsync,
|
|
45
47
|
parseTargetSelector,
|
|
46
48
|
parseWatchOption,
|
|
47
49
|
resolveDeployConfigPath,
|
|
48
50
|
resolveDeployTargetPlaces,
|
|
51
|
+
resolveWatchDelivery,
|
|
49
52
|
toManifestPlaceInfo,
|
|
50
53
|
type BasePlaceResolver,
|
|
51
54
|
type DeployConfig,
|
|
55
|
+
type WatchMode,
|
|
52
56
|
} from '@quenty/nevermore-deploy';
|
|
53
57
|
import {
|
|
54
58
|
registerWatchesAsync,
|
|
@@ -77,14 +81,10 @@ const MULTI_PLACE_CONCURRENCY = 10;
|
|
|
77
81
|
/**
|
|
78
82
|
* Which base places a local watch polls.
|
|
79
83
|
*
|
|
80
|
-
* Deliberately looser than the cloud plan
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* has Open Cloud credentials and can.
|
|
85
|
-
*
|
|
86
|
-
* An exact version pin is still excluded, for the same reason as always: it
|
|
87
|
-
* means "hold this still", so there is nothing to follow.
|
|
84
|
+
* Deliberately looser than the cloud plan, in exactly one way: `watch` is a
|
|
85
|
+
* dispatch address, nothing is dispatched locally, so requiring it would only
|
|
86
|
+
* stop a developer hot-reloading a place that never intends to use CI.
|
|
87
|
+
* Everything about the base place itself is the shared rule.
|
|
88
88
|
*/
|
|
89
89
|
async function _buildLocalWatchEntriesAsync(
|
|
90
90
|
config: DeployConfig,
|
|
@@ -95,21 +95,19 @@ async function _buildLocalWatchEntriesAsync(
|
|
|
95
95
|
const entries: LocalWatchEntry[] = [];
|
|
96
96
|
for (const place of resolveDeployTargetPlaces(config, targetName)) {
|
|
97
97
|
const basePlace = place.basePlace;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
) {
|
|
98
|
+
// The same rule a registration uses, asked with `credentialed: true`: this
|
|
99
|
+
// machine holds the Open Cloud key, so "saved" is readable here even when
|
|
100
|
+
// the service could not see it. Only the workflow half differs, and a local
|
|
101
|
+
// watch has nothing to dispatch.
|
|
102
|
+
const check = checkBasePlaceWatchable(basePlace, { credentialed: true });
|
|
103
|
+
if (basePlace == null || !check.watchable) {
|
|
105
104
|
continue;
|
|
106
105
|
}
|
|
107
|
-
const versionType = basePlace.version ?? 'published';
|
|
108
106
|
entries.push({
|
|
109
107
|
label: formatTargetSelector({ targetName, placeName: place.name }),
|
|
110
108
|
universeId: basePlace.universeId,
|
|
111
109
|
placeId: basePlace.placeId,
|
|
112
|
-
versionType,
|
|
110
|
+
versionType: check.versionType,
|
|
113
111
|
baseline: await resolver.peekAsync(packagePath, basePlace),
|
|
114
112
|
});
|
|
115
113
|
}
|
|
@@ -130,6 +128,7 @@ export interface DeployArgs extends NevermoreGlobalArgs {
|
|
|
130
128
|
output?: string;
|
|
131
129
|
logs?: boolean;
|
|
132
130
|
watch?: string;
|
|
131
|
+
watchMode?: WatchMode;
|
|
133
132
|
watchShareApiKey?: boolean;
|
|
134
133
|
watchUseGhAuth?: boolean;
|
|
135
134
|
}
|
|
@@ -303,6 +302,16 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
303
302
|
'Takes the register endpoint URL, ending in the lease: https://<watch-service>/v1/register/7d',
|
|
304
303
|
type: 'string',
|
|
305
304
|
})
|
|
305
|
+
.option('watch-mode', {
|
|
306
|
+
describe:
|
|
307
|
+
'How the watch reaches whoever rebuilds: "dispatch" fires the workflow, ' +
|
|
308
|
+
'"notify" holds a stream here and rebuilds in this terminal. ' +
|
|
309
|
+
'Defaults to "auto", which dispatches on GitHub Actions and notifies elsewhere — ' +
|
|
310
|
+
'set it explicitly in any other automated context, where notifying would hang the run.',
|
|
311
|
+
type: 'string',
|
|
312
|
+
choices: WATCH_MODES,
|
|
313
|
+
default: 'auto',
|
|
314
|
+
})
|
|
306
315
|
.option('watch-use-gh-auth', {
|
|
307
316
|
describe:
|
|
308
317
|
'Let the GitHub CLI supply the watch token when no environment variable does. ' +
|
|
@@ -420,7 +429,11 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
420
429
|
// registration skips those.
|
|
421
430
|
await registerWatchesAsync({
|
|
422
431
|
option: watchOption,
|
|
423
|
-
monitorName:
|
|
432
|
+
monitorName: buildWatchMonitorName({
|
|
433
|
+
packageName,
|
|
434
|
+
targetName: watchedTarget,
|
|
435
|
+
dryrun: true,
|
|
436
|
+
}),
|
|
424
437
|
resolver: createLockOnlyBasePlaceResolver(),
|
|
425
438
|
useGhAuth: args.watchUseGhAuth,
|
|
426
439
|
triggerAfterRegister: true,
|
|
@@ -636,7 +649,7 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
636
649
|
// Only after a clean deploy: what happens next records the base place
|
|
637
650
|
// version that just shipped, which is not a fact yet if the deploy failed.
|
|
638
651
|
if (watchOption && first.failed === 0) {
|
|
639
|
-
if (isCI()) {
|
|
652
|
+
if (resolveWatchDelivery(args.watchMode, isCI()) === 'dispatch') {
|
|
640
653
|
// The monitor is named for the whole target, so it has to carry every
|
|
641
654
|
// place in that target — not just the ones this invocation deployed.
|
|
642
655
|
// Re-registering replaces its watch list, so a narrowed selector
|
|
@@ -644,9 +657,10 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
644
657
|
// places' watches. Places that did not deploy keep their lock baseline.
|
|
645
658
|
watchResult = await registerWatchesAsync({
|
|
646
659
|
option: watchOption,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
660
|
+
monitorName: buildWatchMonitorName({
|
|
661
|
+
packageName,
|
|
662
|
+
targetName: watchedTarget,
|
|
663
|
+
}),
|
|
650
664
|
resolver: first.resolver,
|
|
651
665
|
useGhAuth: args.watchUseGhAuth,
|
|
652
666
|
robloxApiKey: args.watchShareApiKey ? apiKey : undefined,
|
|
@@ -680,7 +694,10 @@ export class DeployCommand<T> implements CommandModule<T, DeployArgs> {
|
|
|
680
694
|
|
|
681
695
|
const notify = await tryRegisterNotifyWatchesAsync({
|
|
682
696
|
option: watchOption,
|
|
683
|
-
monitorName:
|
|
697
|
+
monitorName: buildWatchMonitorName({
|
|
698
|
+
packageName,
|
|
699
|
+
targetName: watchedTarget,
|
|
700
|
+
}),
|
|
684
701
|
entries: localEntries,
|
|
685
702
|
useGhAuth: args.watchUseGhAuth,
|
|
686
703
|
robloxApiKey: args.watchShareApiKey ? apiKey : undefined,
|
|
@@ -150,8 +150,7 @@ export abstract class BaseJobContext implements JobContext {
|
|
|
150
150
|
// Waiter: the in-flight build was started under a different
|
|
151
151
|
// packageName, so emit our own 'building' phase change for the reporter.
|
|
152
152
|
const resolvedName =
|
|
153
|
-
options.packageName ??
|
|
154
|
-
path.basename(options.packagePath);
|
|
153
|
+
options.packageName ?? path.basename(options.packagePath);
|
|
155
154
|
this._reporter.onPackagePhaseChange(resolvedName, 'building');
|
|
156
155
|
return existing.promise;
|
|
157
156
|
}
|
|
@@ -502,10 +502,14 @@ export class OpenCloudClient {
|
|
|
502
502
|
|
|
503
503
|
/**
|
|
504
504
|
* Resolve the newest saved or published version number of a place, via the
|
|
505
|
-
* Open Cloud Assets API (`asset:read` scope
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
505
|
+
* Open Cloud Assets API (`asset:read` scope). The returned number is the same
|
|
506
|
+
* value the Asset Delivery API's `/version/{n}` route expects, so it can be
|
|
507
|
+
* written straight into a `basePlace.version` pin.
|
|
508
|
+
*
|
|
509
|
+
* This used to add that `legacy-asset:manage` grants `asset:read` too. They
|
|
510
|
+
* are separate boxes on the credentials page and nothing here ever verified
|
|
511
|
+
* the implication, so the claim is gone rather than restated — it had already
|
|
512
|
+
* been copied into the `nevermore login` instructions as fact once.
|
|
509
513
|
*
|
|
510
514
|
* `saved` is the newest version of any kind — Studio saves and publishes
|
|
511
515
|
* share one increasing version sequence. `published` is the newest version
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import {
|
|
3
3
|
describeMissingWatchToken,
|
|
4
4
|
parseGithubRepository,
|
|
@@ -35,6 +35,15 @@ describe('tryResolveGithubDispatchContext token', () => {
|
|
|
35
35
|
'git rev-parse': 'main',
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
// Cleared so these exercise the git fallback rather than whatever the host
|
|
39
|
+
// happens to export. Actions sets both, and on a pull request GITHUB_REF_NAME
|
|
40
|
+
// is a merge ref — which resolution rightly refuses, so without this the
|
|
41
|
+
// whole describe passes locally and fails only in CI.
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.stubEnv('GITHUB_REPOSITORY', '');
|
|
44
|
+
vi.stubEnv('GITHUB_REF_NAME', '');
|
|
45
|
+
});
|
|
46
|
+
|
|
38
47
|
// Registering ships the token to the watch service, so it has to be one the
|
|
39
48
|
// user handed over. Nearly every dev machine has a logged-in `gh`, so reading
|
|
40
49
|
// it by default would turn a local watch into a silent credential send.
|
|
@@ -239,9 +239,11 @@ function _describeFailure(
|
|
|
239
239
|
case 422: {
|
|
240
240
|
// 422 is the service's "your config cannot work" answer, and it covers
|
|
241
241
|
// several unrelated causes now — a missing workflow, an Open Cloud key
|
|
242
|
-
//
|
|
243
|
-
// the wrong vocabulary. Its own message names which, so the only thing
|
|
244
|
-
// worth adding is a remedy that could not already be in it.
|
|
242
|
+
// that cannot read a base place, a "saved" watch with no key, a baseline
|
|
243
|
+
// in the wrong vocabulary. Its own message names which, so the only thing
|
|
244
|
+
// worth adding is a remedy that could not already be in it. The scope
|
|
245
|
+
// itself is deliberately not named here: this file would then be a second
|
|
246
|
+
// place to correct when it is wrong, and it already has been once.
|
|
245
247
|
const workflows = [
|
|
246
248
|
...new Set(
|
|
247
249
|
request.watches.flatMap((w) =>
|