@impeccable/reviews 0.1.22 → 0.1.24
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/dist/fence.js +40 -4
- package/dist/focus.js +17 -0
- package/dist/host-questions.js +2 -2
- package/dist/main.js +27 -10
- package/dist/playbook/cases/cloudflare-previews.md +1 -1
- package/dist/playbook/cases/monorepo.md +5 -5
- package/dist/playbook/cases/netlify-setup.md +3 -3
- package/dist/playbook/cases/preview-protection.md +3 -2
- package/dist/playbook/cases/server-previews.md +3 -3
- package/dist/playbook/cases/sign-in.md +8 -8
- package/dist/playbook/cases/vercel-setup.md +3 -3
- package/dist/playbook.js +18 -3
- package/dist/pr-summary.js +45 -0
- package/dist/preflight.js +2 -1
- package/dist/tools.js +233 -54
- package/dist/ui/look.js +10 -9
- package/package.json +1 -1
package/dist/fence.js
CHANGED
|
@@ -412,7 +412,7 @@ function namesTrackedFiles(paths, cwd) {
|
|
|
412
412
|
const named = new Set(paths.map((a) => resolve(cwd, a)));
|
|
413
413
|
return tracked !== null && tracked.split('\0').filter(Boolean).every((a) => named.has(resolve(cwd, a)));
|
|
414
414
|
}
|
|
415
|
-
function git(args, where, protectedBranches, held = false) {
|
|
415
|
+
function git(args, where, protectedBranches, held = false, ended = new Set()) {
|
|
416
416
|
const words = gitArgs(args);
|
|
417
417
|
if (!words)
|
|
418
418
|
return deny('a git option before the command: run git with the command first');
|
|
@@ -500,6 +500,9 @@ function git(args, where, protectedBranches, held = false) {
|
|
|
500
500
|
const branch = rest[2];
|
|
501
501
|
if (branch.includes(':') || ['HEAD', '@'].includes(pushedBranch(branch)) || protectedBranches.includes(pushedBranch(branch)))
|
|
502
502
|
return deny('a push to the default branch');
|
|
503
|
+
// The person's no ended the setup (#413): of a repository of one app, nothing is pushed; of an app of several, not its own test drive.
|
|
504
|
+
if (ended.has('') || [...ended].some((key) => pushedBranch(branch) === `impeccable/test-drive-${key}`))
|
|
505
|
+
return deny("a push after the person's no ended the setup: there is no preview to review it on");
|
|
503
506
|
// A connected Vercel project builds every push, and calls a new project's first deployment Production, public at the project's own address.
|
|
504
507
|
return held ? deny("a push before impeccable_vercel_connect settled the linked Vercel project's first deployment: run it first") : allow();
|
|
505
508
|
}
|
|
@@ -899,7 +902,7 @@ function simpleCommand(seg, where, opts) {
|
|
|
899
902
|
return deny('a cd that runs only sometimes: put it first, or after ;');
|
|
900
903
|
return isFolder(realPath(resolve(where.cwd, args[0]))) ? allow() : deny('a cd into a folder that is not there');
|
|
901
904
|
case 'git':
|
|
902
|
-
return git(args, where, opts.protectedBranches, opts.hosts.has('vercel') && !(opts.vercelSettled && opts.vercelSettled.projectId === (linkedVercelProject(where.root)?.projectId ?? null)));
|
|
905
|
+
return git(args, where, opts.protectedBranches, opts.hosts.has('vercel') && !(opts.vercelSettled && opts.vercelSettled.projectId === (linkedVercelProject(where.root)?.projectId ?? null)), opts.ended);
|
|
903
906
|
case 'gh':
|
|
904
907
|
// gh reads the file outside the sandbox, after the fence read it: a command still running in the sandbox can swap it in between.
|
|
905
908
|
if (args[0] === 'pr' && args[1] === 'create')
|
|
@@ -1202,7 +1205,7 @@ function unsyncedWorkflows(where, named, carried, synced) {
|
|
|
1202
1205
|
}
|
|
1203
1206
|
/** The fence's answer for one shell command run from the checkout's top folder. */
|
|
1204
1207
|
export function classifyCommand(command, root, options = {}) {
|
|
1205
|
-
const opts = { protectedBranches: options.protectedBranches ?? ['main', 'master'], scriptsChanged: options.scriptsChanged ?? false, hosts: options.hosts ?? new Set(), installs: options.installs ?? new Set(), teams: options.teams ?? new Map(), vercelSettled: options.vercelSettled ?? null, repository: options.repository };
|
|
1208
|
+
const opts = { protectedBranches: options.protectedBranches ?? ['main', 'master'], scriptsChanged: options.scriptsChanged ?? false, hosts: options.hosts ?? new Set(), installs: options.installs ?? new Set(), teams: options.teams ?? new Map(), vercelSettled: options.vercelSettled ?? null, ended: options.ended ?? new Set(), repository: options.repository };
|
|
1206
1209
|
const segments = parseCommand(command);
|
|
1207
1210
|
if (!segments) {
|
|
1208
1211
|
// A here-document is how a file or a message gets written in one command: the write tool, -m and --body do that.
|
|
@@ -1628,6 +1631,22 @@ export function previewRetryState(log) {
|
|
|
1628
1631
|
const setupBranch = (branch) => /^impeccable\/setup(?:-(?:[2-9]|[1-9][0-9]+))?$/.test(branch);
|
|
1629
1632
|
/** The test drive's branch: impeccable/test-drive, an app's impeccable/test-drive-<app key> on a repository with several, each with -2, -3 where an earlier run took it. */
|
|
1630
1633
|
const driveBranch = (branch) => /^impeccable\/test-drive(?:-[A-Za-z0-9][A-Za-z0-9._-]*)?$/.test(branch) && !branch.includes('..') && !branch.endsWith('.') && !branch.endsWith('.lock');
|
|
1634
|
+
/** Under --signed-in-test-drive, why a test drive's branch may not be pushed or opened yet: its app has no test account saved (#410). An app's branch is impeccable/test-drive-<app key>, with -2, -3 where an earlier run took it; a run that sets up one app of several uses impeccable/test-drive. */
|
|
1635
|
+
function signedInRefusal(state, branch) {
|
|
1636
|
+
const signedIn = state.signedIn;
|
|
1637
|
+
if (!signedIn)
|
|
1638
|
+
return;
|
|
1639
|
+
const rest = branch.slice('impeccable/test-drive'.length);
|
|
1640
|
+
const picked = signedIn.picked ?? [];
|
|
1641
|
+
const named = (key) => rest === `-${key}` || (rest.startsWith(`-${key}-`) && /^\d+$/.test(rest.slice(key.length + 2)));
|
|
1642
|
+
// An app's own branch, a numeric key's too, else the unnamed branch of a run that picked one app.
|
|
1643
|
+
const saved = !signedIn.apps ? signedIn.saved.has('')
|
|
1644
|
+
: picked.some((key) => named(key)) ? picked.some((key) => named(key) && signedIn.saved.has(key))
|
|
1645
|
+
: /^(?:-\d+)?$/.test(rest) && picked.length === 1 && signedIn.saved.has(picked[0]);
|
|
1646
|
+
if (saved)
|
|
1647
|
+
return;
|
|
1648
|
+
return deny(`the run was started with --signed-in-test-drive, and no test account is saved${signedIn.apps ? ' for this app' : ''}, so its test drive cannot change a page behind the sign-in. Save the accounts first, as the sign-in case says. Where the app has no sign-in of its own, or every account was skipped, push and open no test drive: say in the report that --signed-in-test-drive could not be honoured, and why`);
|
|
1649
|
+
}
|
|
1631
1650
|
/** A GitHub repository as `<owner>/<name>`. */
|
|
1632
1651
|
const REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?\/[A-Za-z0-9_.-]{1,100}$/;
|
|
1633
1652
|
/** `gh pr ready <the test drive's branch> [--repo <owner>/<name>]`, and nothing else (never `--undo`, a number or another branch): the branch and the repository named, or null. */
|
|
@@ -1728,6 +1747,9 @@ export function reservePreviewCommand(state, command, root, scratch) {
|
|
|
1728
1747
|
// The test drive is the run's other pull request (one an app on a repository with several): one branch, one pull request, a few pushes, none of the setup pull request's attempts.
|
|
1729
1748
|
const drivePush = pushBranch(gitWords, driveBranch);
|
|
1730
1749
|
if (drivePush) {
|
|
1750
|
+
const unsigned = signedInRefusal(state, drivePush);
|
|
1751
|
+
if (unsigned)
|
|
1752
|
+
return unsigned;
|
|
1731
1753
|
const drive = driveOf(drivePush);
|
|
1732
1754
|
if (!drive)
|
|
1733
1755
|
return deny(state.drivesAllowed > 1 ? `one test drive for each of the ${state.drivesAllowed} apps this run sets up, each on one branch` : 'keep the test drive on one branch');
|
|
@@ -1761,6 +1783,9 @@ export function reservePreviewCommand(state, command, root, scratch) {
|
|
|
1761
1783
|
|| (!parsed.repository && realPath(where.cwd) !== where.root))
|
|
1762
1784
|
return deny('name this repository explicitly when creating a pull request');
|
|
1763
1785
|
if (parsed.head && driveBranch(parsed.head)) {
|
|
1786
|
+
const unsigned = signedInRefusal(state, parsed.head);
|
|
1787
|
+
if (unsigned)
|
|
1788
|
+
return unsigned;
|
|
1764
1789
|
const drive = driveOf(parsed.head);
|
|
1765
1790
|
if (!drive)
|
|
1766
1791
|
return deny(state.drivesAllowed > 1 ? `one test drive for each of the ${state.drivesAllowed} apps this run sets up, each opened from the branch that was pushed` : 'open the test drive from the branch that was pushed');
|
|
@@ -2000,6 +2025,17 @@ export function settleVercelFirstDeployment(log, settlement) {
|
|
|
2000
2025
|
export function vercelFirstDeploymentSettled(log) {
|
|
2001
2026
|
return settledVercel.get(log) ?? null;
|
|
2002
2027
|
}
|
|
2028
|
+
/** The apps whose setup the person's no ended (#413), by the run's log: '' for a repository of one app, which then pushes nothing; an app's key, whose own test drive is not pushed. */
|
|
2029
|
+
const endedSetups = new WeakMap();
|
|
2030
|
+
function setupsEnded(log) {
|
|
2031
|
+
let keys = endedSetups.get(log);
|
|
2032
|
+
if (!keys)
|
|
2033
|
+
endedSetups.set(log, (keys = new Set()));
|
|
2034
|
+
return keys;
|
|
2035
|
+
}
|
|
2036
|
+
export function endAppSetup(log, key) {
|
|
2037
|
+
setupsEnded(log).add(key);
|
|
2038
|
+
}
|
|
2003
2039
|
/** Tool calls a run makes before the fence refuses more: a setup takes far fewer, and a run steered elsewhere stops there. */
|
|
2004
2040
|
export const MAX_TOOL_CALLS = 400;
|
|
2005
2041
|
/** What still runs past the cap, so the run can say where it got to and end. */
|
|
@@ -2125,7 +2161,7 @@ export function fenceExtension(root, log, protectedBranches, scratch, scans = sc
|
|
|
2125
2161
|
let shown;
|
|
2126
2162
|
if (event.toolName === 'bash') {
|
|
2127
2163
|
shown = String(input.command ?? '');
|
|
2128
|
-
decision = classifyCommand(shown, root, { protectedBranches, scriptsChanged, scratch, hosts: hostsAgreed(log), installs: installsAgreed(log), teams: teamsChosen(log), vercelSettled: vercelFirstDeploymentSettled(log), touched, synced, repository: retry.repository });
|
|
2164
|
+
decision = classifyCommand(shown, root, { protectedBranches, scriptsChanged, scratch, hosts: hostsAgreed(log), installs: installsAgreed(log), teams: teamsChosen(log), vercelSettled: vercelFirstDeploymentSettled(log), ended: setupsEnded(log), touched, synced, repository: retry.repository });
|
|
2129
2165
|
if (decision.verdict === 'allow')
|
|
2130
2166
|
decision = reservePreviewCommand(retry, shown, root, scratch);
|
|
2131
2167
|
if (decision.verdict === 'allow') {
|
package/dist/focus.js
CHANGED
|
@@ -32,6 +32,23 @@ const hasApps = (repo) => Boolean(record(repo.items)?.[APPS.item]) || records(re
|
|
|
32
32
|
export function severalApps(answer) {
|
|
33
33
|
return reposOf(answer).some(hasApps);
|
|
34
34
|
}
|
|
35
|
+
/** The state's sign-in item of `repo`, which it has only when the scan read a sign-in of the app's own. */
|
|
36
|
+
export const signInItem = (state, repo) => record(record(reposOf(state).find((r) => String(r.name).toLowerCase() === repo.toLowerCase())?.items)?.signin);
|
|
37
|
+
/**
|
|
38
|
+
* The apps of `repo` with a test account saved, as the sign-in item says (#410): '' on a repository of one app. On one
|
|
39
|
+
* with several, the item speaks for its first picked app with that app's roles, and the others ride along in `perApp`
|
|
40
|
+
* with their status alone, `done` once an account is saved.
|
|
41
|
+
*/
|
|
42
|
+
export function accountsSaved(state, repo) {
|
|
43
|
+
const item = signInItem(state, repo);
|
|
44
|
+
if (!item)
|
|
45
|
+
return [];
|
|
46
|
+
const saved = Array.isArray(item.rolesSaved) && item.rolesSaved.length > 0;
|
|
47
|
+
if (!severalApps(state))
|
|
48
|
+
return saved ? [''] : [];
|
|
49
|
+
const keys = records(item.perApp).filter((app) => app.status === 'done').map((app) => app.app);
|
|
50
|
+
return [...new Set([...(saved ? [item.app] : []), ...keys])].filter((key) => typeof key === 'string' && key !== '');
|
|
51
|
+
}
|
|
35
52
|
/**
|
|
36
53
|
* Whether the state or the checkup shows a card of this setup `not-selected` on `repo`, a repository with several apps.
|
|
37
54
|
* The command's token covers the apps there were when the person signed in, so this is an app the scan proposed later.
|
package/dist/host-questions.js
CHANGED
|
@@ -306,7 +306,7 @@ export function vercelBypassPaste(stopped) {
|
|
|
306
306
|
: `Make one yourself: ${where} → Create, named "Impeccable reviews".`;
|
|
307
307
|
return { ...HOST_SCREENS['vercel-bypass-key'], prompt: `Vercel keeps your previews behind a login. ${stopped.reason} ${next} It is stored encrypted and used only to open previews.` };
|
|
308
308
|
}
|
|
309
|
-
/** The apps question on a repository with several apps, worded here: the tool lists the apps the state has, each picked as it is on, so every run asks it the same way. */
|
|
309
|
+
/** The apps question on a repository with several apps, worded here: the tool lists the apps the state has, each picked as it is on, so every run asks it the same way. It has no Skip: at least one app is picked (#405). */
|
|
310
310
|
export const APPS_QUESTION = {
|
|
311
311
|
id: 'apps',
|
|
312
312
|
kind: 'multi',
|
|
@@ -316,7 +316,7 @@ export const APPS_QUESTION = {
|
|
|
316
316
|
};
|
|
317
317
|
/** The apps question's hint when the command was started below the repository's top: `where` is an app's folder ("Tally Board's folder") or the folder itself. */
|
|
318
318
|
export const startedIn = (where) => `You started in ${where}. Impeccable sets up from the top of the repository.`;
|
|
319
|
-
/** The apps question when the command was started for one app (`--app`): no list, one confirm screen naming that app, and the list behind
|
|
319
|
+
/** The apps question when the command was started for one app (`--app`): no list, one confirm screen naming that app, and the list behind Pick other apps for someone who wants another app (#286); there is no Skip (#405). */
|
|
320
320
|
export const namedAppQuestion = (app) => ({
|
|
321
321
|
id: APPS_QUESTION.id,
|
|
322
322
|
kind: 'confirm',
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import { dirname, join } from 'node:path';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { runAgent } from './agent.js';
|
|
7
7
|
import { fenceExtension, guardedFileOperations, hostsAgreed, placeOf, previewRetryState } from './fence.js';
|
|
8
|
-
import { focusInstructions, focusState, severalApps } from './focus.js';
|
|
8
|
+
import { accountsSaved, focusInstructions, focusState, severalApps, signInItem } from './focus.js';
|
|
9
9
|
import { requestJson } from './http.js';
|
|
10
10
|
import { copyText, letTabsHear, openBrowser, progressPage, screenPrompt, setupAuth, signIn } from './login.js';
|
|
11
11
|
import { createLog, UserError } from './log.js';
|
|
@@ -16,6 +16,7 @@ import { scanRecorder } from './scan.js';
|
|
|
16
16
|
import { createReporter, reportingOff } from './report.js';
|
|
17
17
|
import { createSessionSync, pageStatus, plainReport, syncExtension } from './sync.js';
|
|
18
18
|
import { checkPushAccess, readPushAccess } from './push-access.js';
|
|
19
|
+
import { askPrSummary, PR_SUMMARY_OFF } from './pr-summary.js';
|
|
19
20
|
import { appsSummary, createTools, ending, ownPullRequest, resultLine } from './tools.js';
|
|
20
21
|
import { transcriptPaths } from './transcript.js';
|
|
21
22
|
import { askPerson } from './ui/bridge.js';
|
|
@@ -109,7 +110,7 @@ export async function main(argv) {
|
|
|
109
110
|
// Printed as the command exits, after the screen has gone, so it stays in the scrollback.
|
|
110
111
|
if (log.path)
|
|
111
112
|
atExit(() => [`Debug log: ${log.path}`]);
|
|
112
|
-
log.write('start', { host: flags.host, model: flags.model, effort: flags.effort, yes: flags.yes, node: process.versions.node });
|
|
113
|
+
log.write('start', { host: flags.host, model: flags.model, effort: flags.effort, yes: flags.yes, signedInTestDrive: flags.signedInTestDrive, node: process.versions.node });
|
|
113
114
|
if (flags.frames)
|
|
114
115
|
recordFrames(flags.frames);
|
|
115
116
|
const checkout = readCheckout(flags.installDir?.path ?? process.cwd(), flags.installDir?.from);
|
|
@@ -226,19 +227,32 @@ export async function main(argv) {
|
|
|
226
227
|
if (flags.app && severalApps(state.data) && !appKeys.includes(flags.app)) {
|
|
227
228
|
throw new UserError(`--app ${flags.app} is not an app of ${slug}. Its apps are ${appKeys.join(', ')}. Run it again with one of them, or without --app.`);
|
|
228
229
|
}
|
|
230
|
+
// --signed-in-test-drive changes a page behind the app's own sign-in: with none found, it can't be honoured, and nothing is set up (#410).
|
|
231
|
+
if (flags.signedInTestDrive && !signInItem(state.data, token.repository.name)) {
|
|
232
|
+
await nothingToSetUp(liveSync, reporter, 'failed', 'No sign-in found', `The run was started with --signed-in-test-drive, but Impeccable found no sign-in of the app's own in ${slug}, so the test drive can't change a page behind one. Nothing was set up.`, [webPage]);
|
|
233
|
+
}
|
|
229
234
|
// The setup pushes as the person, so whether git may push is asked of GitHub now, before anything is created.
|
|
230
|
-
|
|
235
|
+
// A question asked before the agent starts. Ctrl+C there answers it as closed while stopEarly winds the run down and
|
|
236
|
+
// exits, so nothing after it runs: no update over the stop, and no agent started.
|
|
237
|
+
const askUpFront = async (screen) => {
|
|
231
238
|
liveSync.update({ phase: 'waiting', question: { id: screen.id, prompt: screen.prompt } });
|
|
232
239
|
const answer = await askPerson({ ...screen, skippable: true });
|
|
240
|
+
if (stopping)
|
|
241
|
+
return new Promise(() => { });
|
|
233
242
|
liveSync.update({ phase: 'running', question: null });
|
|
234
243
|
return answer;
|
|
235
|
-
}
|
|
244
|
+
};
|
|
245
|
+
const push = await checkPushAccess(slug, () => readPushAccess(checkout.root), askUpFront);
|
|
236
246
|
log.write('push access', push.outcome);
|
|
237
247
|
if (push.outcome === 'refused' || push.outcome === 'signed-out') {
|
|
238
248
|
await nothingToSetUp(liveSync, reporter, 'stopped', "Git can't push", `Git can't push to ${slug}${push.account ? ` as ${push.account}` : ''}, so nothing was set up. Sign git in as an account that can, then run this again.`, [webPage]);
|
|
239
249
|
}
|
|
240
250
|
if (!interactive)
|
|
241
251
|
say('Signed in.\n');
|
|
252
|
+
// The summary with before and after in each pull request's description, asked with the other consents up front, so the test drive's has it too (#402).
|
|
253
|
+
const reviewSettings = `${setupApi}/repos/${encodeURIComponent(token.repository.name)}/review-settings`;
|
|
254
|
+
const prSummary = await askPrSummary(() => requestJson(reviewSettings, { token: token.access_token }, log), askUpFront, (body) => requestJson(reviewSettings, { method: 'PUT', token: token.access_token, body }, log));
|
|
255
|
+
log.write('pr summary', prSummary);
|
|
242
256
|
// Beside the transcript, named after the run as it is, so a run never writes over another's in the temp folder.
|
|
243
257
|
const scans = scanRecorder(log, flags.scanReport ? join(dirname(transcriptPaths(run.run_id, flags.transcript).markdown), `scan-report-${run.run_id.replace(/[^\w.-]/g, '_')}.json`) : undefined);
|
|
244
258
|
atExit(() => scans.report());
|
|
@@ -279,6 +293,8 @@ export async function main(argv) {
|
|
|
279
293
|
// On a repository with several apps, "You're all set up" means every app this run picked is: one left unproven is named, with its one step (#286). A run short of that says what is not, and exits 1 (#321).
|
|
280
294
|
const apps = appsSummary(report.apps ?? []);
|
|
281
295
|
const end = ending(report);
|
|
296
|
+
// The person's no left no app with a preview Impeccable can open: the screen says why alone, with Close, and the report follows it here (#413).
|
|
297
|
+
const ended = Boolean(report.ended);
|
|
282
298
|
// One pull request a screen, each opened in the browser and put on the clipboard as its screen appears: the test drive (each app's, on a repository with several), then the setup pull request.
|
|
283
299
|
const hand = { open: openBrowser, copy: copyText };
|
|
284
300
|
if (drives)
|
|
@@ -289,18 +305,18 @@ export async function main(argv) {
|
|
|
289
305
|
await showOutro({
|
|
290
306
|
outcome: end.outcome,
|
|
291
307
|
title: end.title,
|
|
292
|
-
result: [resultLine({ ...report, setupPr }), apps.line].filter(Boolean).join(' '),
|
|
308
|
+
result: ended ? resultLine(report) : [resultLine({ ...report, setupPr }), apps.line].filter(Boolean).join(' '),
|
|
293
309
|
// Each app's test drive had its own screen: the last one holds the setup pull request, or lists them all.
|
|
294
|
-
link: (setupPr ?? (drives ? undefined : testPr))?.url,
|
|
295
|
-
links: [
|
|
310
|
+
link: ended ? undefined : (setupPr ?? (drives ? undefined : testPr))?.url,
|
|
311
|
+
links: ended ? [] : [
|
|
296
312
|
...(drives ? drives.map((drive) => ({ label: `${drive.name}'s test drive, watch the review there`, url: drive.url })) : testPr ? [{ label: 'Test drive, watch the review there', url: testPr.url }] : []),
|
|
297
313
|
// Without a current test drive the setup pull request is not ready to merge; the report says why.
|
|
298
314
|
...(setupPr ? [{ label: testPr ? 'Setup pull request, merge this' : 'Setup pull request', url: setupPr.url }] : []),
|
|
299
315
|
{ label: 'Web page', url: page },
|
|
300
316
|
],
|
|
301
317
|
todo: end.todo,
|
|
302
|
-
...(report.hosts?.length ? { used: report.hosts } : {}),
|
|
303
|
-
warnings: [...removed, ...left, ...(await unopened(drives ? drives.length : testPr ? 1 : 0)), ...(saved && !saved.ok ? [`The web page missed the last update (${saved.error}), so it may lag behind this.`] : [])],
|
|
318
|
+
...(report.hosts?.length && !ended ? { used: report.hosts } : {}),
|
|
319
|
+
warnings: [...removed, ...left, ...(await unopened(drives ? drives.length : testPr ? 1 : 0)), ...(saved && !saved.ok ? [`The web page missed the last update (${saved.error}), so it may lag behind this.`] : []), ...(prSummary === 'off' && !ended ? [PR_SUMMARY_OFF] : [])],
|
|
304
320
|
}, hand);
|
|
305
321
|
// The web page has the report; when it missed it, or the run fell short of all set up, whose why it holds, the report stays here as well.
|
|
306
322
|
if (!saved?.ok || end.code)
|
|
@@ -339,7 +355,7 @@ export async function main(argv) {
|
|
|
339
355
|
effort: flags.effort,
|
|
340
356
|
gatewayUrl: run.gateway_url,
|
|
341
357
|
runToken,
|
|
342
|
-
systemPrompt: playbook({ owner: checkout.owner, name: token.repository.name, scratch, sandboxed: !sandbox.warning }),
|
|
358
|
+
systemPrompt: playbook({ owner: checkout.owner, name: token.repository.name, scratch, sandboxed: !sandbox.warning, signedInTestDrive: flags.signedInTestDrive }),
|
|
343
359
|
firstMessage: firstMessage({ owner: checkout.owner, name: token.repository.name }),
|
|
344
360
|
firstContext: setupContext(focused, focusState(state.data)),
|
|
345
361
|
tools: createTools({
|
|
@@ -360,6 +376,7 @@ export async function main(argv) {
|
|
|
360
376
|
severalApps: severalApps(state.data),
|
|
361
377
|
...(flags.app ? { app: flags.app } : {}),
|
|
362
378
|
start: checkout.start,
|
|
379
|
+
...(flags.signedInTestDrive ? { signedInTestDrive: { accountsSaved: accountsSaved(state.data, token.repository.name) } } : {}),
|
|
363
380
|
}),
|
|
364
381
|
extensions: [
|
|
365
382
|
fenceExtension(checkout.root, log, checkout.protectedBranches, scratch, scans),
|
|
@@ -17,7 +17,7 @@ Cloudflare Pages deploys previews of the pull requests: impeccable_cloudflare_pr
|
|
|
17
17
|
2. **Not connected** (`cause: "source"`): the Pages project the checkout names (its wrangler config, or the README's pages.dev address) exists with no Git repository connected, so no push builds. impeccable_cloudflare_connect connects it to this repository.
|
|
18
18
|
3. **A Worker, not a Pages project** (`worker` in the answer, and no `pullRequestPreviews`): Cloudflare builds the repository as a Worker (Workers Builds: its check runs name the script, or the wrangler config or the README's workers.dev address does), which gives pull requests no preview, and a Worker with static assets no branch build at all. impeccable_cloudflare_connect, with the build command and output folder the checkout runs, creates a Pages project for the repository and connects it; the Worker stays as it is. The same tool creates the project where the config names a Pages project cf finds nowhere.
|
|
19
19
|
4. **Cloudflare's GitHub app cannot see the repository** (`githubApp` in the answer: the app's installation covers selected repositories, or `installed: false`): a project connected with previews on still gets no build, since the app builds only the repositories its installation names. Nothing in the account shows it, so the wait for the test drive's build tells: when none comes, the wait's own screen opens the installation page (`githubApp.page`) and waits again on the person's yes. Its `reason` says when they skipped it or no build came after the retries.
|
|
20
|
-
- **`skipped` or `refused` from any of these**: push no test drive on Cloudflare (pull-requests' "No preview, no test drive" where a preview was proven; else set the preview up as server-previews says, never without saying so) and say under "What was skipped, and why" which cause stopped it, with the `reason`, and name the `page` where there is one; the person was asked in this run, so `left` stays empty.
|
|
20
|
+
- **`skipped` or `refused` from any of these**: push no test drive on Cloudflare (pull-requests' "No preview, no test drive" where a preview was proven; else set the preview up as server-previews says, never without saying so) and say under "What was skipped, and why" which cause stopped it, with the `reason`, and name the `page` where there is one; the person was asked in this run, so `left` stays empty. `skipped`, or the GitHub app's access skipped on the wait's screen, with no method saved, is a no that ends the setup, as the playbook says: no other preview is set up.
|
|
21
21
|
- **No `pullRequestPreviews`, no `worker`** (`cli` signed out or unknown, or no Pages project built from this repository among the accounts cf reaches): the check could not be made. Say so in one line under "What is not verified", naming why, and go on as the aim says: nothing listed, as "Nothing listed" says; a preview listed, as the steps say, where the wait for the test drive's build then tells.
|
|
22
22
|
- **Nothing listed**, and neither `pullRequestPreviews` nor `worker` nor a project the checkout names: Cloudflare's previews are not on record, so set the preview up as server-previews says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
|
|
23
23
|
- **The pull requests**: no setup pull request; the test drive branches from the default branch, and Cloudflare builds every pushed branch, so pull-requests' step 4 goes the Vercel way with impeccable_cloudflare_previews in place of impeccable_hosted_previews: with `branch: "impeccable/test-drive"` it waits up to five minutes for the check run on the pushed head, under a status line with its timer, and the test drive's page is proven on the alias it lists before the pull request opens. In place of pull-requests' step 3, build the test drive branch once with the build Cloudflare runs (the Pages project's build command, which the checkout's `package.json` `build` script usually is), so a page that does not build is never pushed.
|
|
@@ -5,13 +5,13 @@ The state's repository has an `apps` item: the scan proposed several apps, folde
|
|
|
5
5
|
### Steps
|
|
6
6
|
|
|
7
7
|
1. **Confirm the proposal** as it stands, while the item's status is `needs-you`: impeccable_write to `apps` with the `confirm` action the item's instructions give. Its answer lists every app of the repository, on and off, with its `key`, `name`, `pathPrefixes` and `enabled`; the one the scan switched on is the repository's default. An item that is `done` or `skipped` was answered before (on the setup page, or by an earlier run): no confirm, and step 2 all the same. Done when it answered `apps`, or the item was answered before.
|
|
8
|
-
2. **Ask which apps this run sets up**, on every run, with impeccable_ask: `id: "apps"`, `kind: "multi"`, and the `prompt` *"Which apps should get Impeccable previews?"*. The tool words the question and lists the state's apps (the repository's own, `main`, only where the Worker kept it as a product of its own), each picked as it is on, or every one after this run's confirm. Mark the Set up monorepo apps task in progress first. Every write and host tool for an app is refused until it is answered. Done when it answered `values
|
|
9
|
-
3. **Write the picks**, one action a call as the item's instructions give them: `enable` each chosen app that is off, then `disable` each app that is on and was not chosen. The last app that is on cannot be switched off (409), which is why the enables come first. **After `named`**, `enable` that app when it is off, and switch no other off.
|
|
8
|
+
2. **Ask which apps this run sets up**, on every run, with impeccable_ask: `id: "apps"`, `kind: "multi"`, and the `prompt` *"Which apps should get Impeccable previews?"*. The tool words the question and lists the state's apps (the repository's own, `main`, only where the Worker kept it as a product of its own), each picked as it is on, or every one after this run's confirm. Mark the Set up monorepo apps task in progress first. Every write and host tool for an app is refused until it is answered. It has no Skip: the person picks at least one app. Done when it answered `values` or `named` (the command was started for that one app with `--app`, from its card on the dashboard: the tool showed one confirm screen naming that app instead of the list, and the list all the same when the person asked for other apps there). Where no answer came (the input ended), the tool wrote nothing and the run ends there: call nothing more. After `named`, that app is the one this run sets up: every host read, key, proof, method and check is its own, and no other app is mentioned past that screen.
|
|
9
|
+
3. **Write the picks**, one action a call as the item's instructions give them: `enable` each chosen app that is off, then `disable` each app that is on and was not chosen. The last app that is on cannot be switched off (409), which is why the enables come first. **After `named`**, `enable` that app when it is off, and switch no other off. Done when the last answer's `apps` show `enabled` true for every chosen app and false for the rest (after `named`, true for that app).
|
|
10
10
|
4. **Read the state again** with impeccable_state. Done when the preview item's `apps` names each chosen app's `directory`, `build` and `method` (none yet for an app not set up), and `perApp` its status.
|
|
11
|
-
5. **Set each chosen app up**, one after the other, as the playbook's steps 3 and 4 say for that app alone, `appKey` on every write and check (`preview`, `test-accounts`, `latest-pull-check`, `connection-check`): its method by the case that applies to it (server-previews when no host deploys it, a host's case when the host deploys that app as a project of its own, proven on that app's own address: impeccable_hosted_previews, impeccable_netlify_previews and impeccable_cloudflare_previews take its `appKey` and list only the previews of the project or site that builds its folder (impeccable_netlify_connect takes it too, and connects that site), so another app's address, project or key never stands in for it; their `unmatched` says why none can be told, and that app goes as server-previews says, with the reason under "What is not verified"), and its own sign-in when the scan item's `accessByApp` says it has one, each account question's title naming the app (*"Order desk admin account"*). **The lock in front of each app's previews is that app's own**: when its proof answers a `gate`, preview-protection's steps run for that app, with its `appKey`, before any other app's step (the tools refuse the next app's reads and writes until its key tool was called), and the key goes to that app's own project or site; a key saved for one app says nothing about another's, and the state's `keySaved` speaks for one app at most. Where a host's setup case (vercel-setup, netlify-setup) opens and records an app's test drive here, check that app's preview right after the recording, as pull-requests' step 6 says with its `appKey`, while its test drive is still the newest open pull request, before the next app is set up. Done when every chosen app's method is saved, or its preview skipped as server-previews says, and its sign-in saved or skipped, and no other app got a write, a build or a question.
|
|
11
|
+
5. **Set each chosen app up**, one after the other, as the playbook's steps 3 and 4 say for that app alone, `appKey` on every write and check (`preview`, `test-accounts`, `latest-pull-check`, `connection-check`), and on the host question, a host's sign-in and team questions, impeccable_enable_previews and impeccable_cloudflare_connect, so a no ends the setup for that app alone: its method by the case that applies to it (server-previews when no host deploys it, a host's case when the host deploys that app as a project of its own, proven on that app's own address: impeccable_hosted_previews, impeccable_netlify_previews and impeccable_cloudflare_previews take its `appKey` and list only the previews of the project or site that builds its folder (impeccable_netlify_connect takes it too, and connects that site), so another app's address, project or key never stands in for it; their `unmatched` says why none can be told, and that app goes as server-previews says, with the reason under "What is not verified"), and its own sign-in when the scan item's `accessByApp` says it has one, each account question's title naming the app (*"Order desk admin account"*). **The lock in front of each app's previews is that app's own**: when its proof answers a `gate`, preview-protection's steps run for that app, with its `appKey`, before any other app's step (the tools refuse the next app's reads and writes until its key tool was called), and the key goes to that app's own project or site; a key saved for one app says nothing about another's, and the state's `keySaved` speaks for one app at most. Where a host's setup case (vercel-setup, netlify-setup) opens and records an app's test drive here, check that app's preview right after the recording, as pull-requests' step 6 says with its `appKey`, while its test drive is still the newest open pull request, before the next app is set up. Done when every chosen app's method is saved, or its preview skipped as server-previews says, and its sign-in saved or skipped, and no other app got a write, a build or a question.
|
|
12
12
|
6. **The pull requests** as pull-requests says: the setup commit once for the repository, taking the state's one workflow `file` once every chosen app's method and build settings are saved, or its preview skipped as server-previews says. A run that sets up one app opens its test drive as pull-requests says, on `impeccable/test-drive`, its page in that app alone. A run that sets up more opens **a test drive of its own for each chosen app with a preview**, one app after the other, so each app's review shows on its own pull request; an app whose host's setup case (vercel-setup, netlify-setup) already opened, recorded and checked its test drive in step 5 has it, and gets none again. An app's test drive is branch `impeccable/test-drive-<appKey>` from the setup commit (from the default branch when there is none), with its page in that app alone, in that app's own framework and linked from that app's own public page, and no change to any other app; that case's step 3 builds that app as its host's case says; its step 4 pushes it and proves its page on that app's own preview, with that app's key where its lock stands in front; its step 5 opens it with the app's name in the title (*"Impeccable test drive: Storefront (safe to close)"*) and records it with impeccable_test_pr and that app's `appKey` (which the tool needs only when the run sets up more than one app); its step 6 checks that app with its `appKey` while its test drive is the newest open pull request, before the next app's test drive opens. An app whose preview was skipped gets no test drive, and goes under "What was skipped, and why". Step 7, when there is a setup commit, opens the setup pull request once, last, and records it with the last app's test drive and its `appKey` again. Done when every chosen app with a preview has its test drive recorded and checked, or the reason it has none in the report, and pull-requests' step 7 is done.
|
|
13
|
-
- **A chosen app whose preview was skipped** (server-previews): it stays on, with no method, no job in the workflow and no test drive page. It holds nothing back: the other apps' setup commit, test drive and checks go on as above, and its checkup entry left in `next` counts as done for the playbook's step 5. The report says, on its line and under "What was skipped, and why", that it gets no preview until its host deploys them.
|
|
14
|
-
7. **Report per app**: under "What was configured, and by whom", one line per app the question offered: on or off and why (picked, unticked, named with `--app
|
|
13
|
+
- **A chosen app whose preview was skipped** (server-previews), or whose setup ended on a no (the playbook's "A no that ends the setup"): it stays on, with no method, no job in the workflow and no test drive page. It holds nothing back: the other apps' setup commit, test drive and checks go on as above, and its checkup entry left in `next` counts as done for the playbook's step 5. The report says, on its line and under "What was skipped, and why", that it gets no preview until its host deploys them, or which no ended its setup. When no chosen app is left with a preview, call impeccable_report: the last screen says why the setup ended.
|
|
14
|
+
7. **Report per app**: under "What was configured, and by whom", one line per app the question offered: on or off and why (picked, unticked, named with `--app`), its method, its sign-in, what was verified; an app the question did not offer and the state shows off (`enabled` false) was turned off on the setup page, and its line says so, never that it was left off in the question; after `named`, an app still on that this run did not set up is outside this run, not turned off; an app left out is under "What was skipped, and why" as well, with the setup page as where it is turned on. impeccable_report adds a section of its own, "Each app, as this run left it", from what the run saved and proved, and the last screen says "You're all set up" only when every chosen app is: an app whose preview met a lock with no key saved, or got no method, is named there with its one step, so the report's lines say the same. Done when every app is on one line.
|
|
15
15
|
|
|
16
16
|
### Reference
|
|
17
17
|
|
|
@@ -7,10 +7,10 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
|
|
|
7
7
|
1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "netlify-setup"`. The tool does the rest: it shows the setup screen once (a person signed in already confirms the setup with that sign-in on it, with no login to run), runs the login in the browser when the Netlify CLI is signed out and offers a retry. Done when it answered `confirmed`.
|
|
8
8
|
- **`cli: "missing"`**: the CLI was not on this machine, and the person said Install on the tool's screen for it: install it with `npm install -g netlify-cli@27.11.2`, exactly so (the run installs it with npm alone, pinned to that version, only after that Install, and refuses another package manager, version or tag); then ask again. The setup screen shows no second time; an install that did not complete brings the steps for the person's own install up instead.
|
|
9
9
|
- **`teams` in the answer** (the person has several Netlify teams): ask once which owns the site, `kind: "single"`, `id: "netlify-team"`, `options` the teams as given (the tool words the rest), and pass the pick to `sites:create` as `--account-slug`.
|
|
10
|
-
- **Skip**:
|
|
10
|
+
- **Skip**: a no that ends the setup, as the playbook says: say under "What was skipped, and why" that Netlify was found and the sign-in was skipped. The steps below do not apply.
|
|
11
11
|
2. **Link or create the site** (run each Netlify command plain, on its own: a command chained after `sites:create` leaves the site unrecorded): `netlify link --git-remote-url <the GitHub origin>` links a site Netlify already builds from this repository; when it finds none, `netlify sites:create --name <name>` creates one in the person's team and links this folder. Name it after the repository: lowercase letters, digits and hyphens, at most 40 characters; a name Netlify says is taken gets `-2`. Done when `netlify status` names the linked project.
|
|
12
12
|
- **A site already built from this repository** (impeccable_netlify_previews named it in `sites`; on a repository with several apps, read with the app's `appKey`): link nothing, since `netlify link` asks in a terminal this run does not have when several sites are built from the repository. Step 3 finds that site by itself.
|
|
13
|
-
3. **Connect the repository** with impeccable_netlify_connect (with the app's `appKey` on a repository with several apps): it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, when that site is the one this run created or one built from this repository already (a site built from another repository, or one the run did not create, is left as it is, with the reason), and asks the person for the app's access when Netlify cannot read the repository. Give `command` and `publish` only where no `netlify.toml` sets the build. Done when it answered `connected: true`; a site `netlify link` found is connected already, and it says so. **Deploy Previews off** on that site (`pullRequestPreviews` in its answer, or in impeccable_netlify_previews', `enabled: false`): the aim holds, so ask once with impeccable_enable_previews (`host: "netlify"`) before step 4, and never a second time. `enabled`: step 4. `skipped`
|
|
13
|
+
3. **Connect the repository** with impeccable_netlify_connect (with the app's `appKey` on a repository with several apps): it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, when that site is the one this run created or one built from this repository already (a site built from another repository, or one the run did not create, is left as it is, with the reason), and asks the person for the app's access when Netlify cannot read the repository. Give `command` and `publish` only where no `netlify.toml` sets the build. Done when it answered `connected: true`; a site `netlify link` found is connected already, and it says so. **Deploy Previews off** on that site (`pullRequestPreviews` in its answer, or in impeccable_netlify_previews', `enabled: false`): the aim holds, so ask once with impeccable_enable_previews (`host: "netlify"`) before step 4, and never a second time. `enabled`: step 4. `skipped`: a no that ends the setup, as the playbook says, with the `reason` and the `page` in the report. `refused`: the setup ends here, as "A step fails before the test drive is pushed" says, with the `reason` and the `page` in the report, and `left` empty, since the person was asked in this run.
|
|
14
14
|
4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say (on a repository with several apps, where the run sets up more than one, that app's own test drive, on `impeccable/test-drive-<appKey>` with its page in that app alone, as the monorepo case's step 6 says), with the changes netlify-previews' reference gives: from the default branch, no setup pull request, built once with the build Netlify runs. Stage the new page and the page that links to it by name: `netlify sites:create` writes `.netlify/` and may add it to `.gitignore`, and that change stays out of the pull request. Done when `git push -u origin impeccable/test-drive` (the app's own branch, where it has one) succeeded.
|
|
15
15
|
5. **Its draft, its preview proven and saved, then marked ready** as pull-requests' step 4 (Netlify) and step 5 say: the draft, whose deploy preview is the site's first, the wait for it by `pull`, the test drive's page proven on it, impeccable_write to `preview` as netlify-previews' step 2 says, `gh pr ready`, then the recording. The playbook's step 6 then has no pull request left to open: go on with the playbook's step 4. Done when impeccable_test_pr answered "Recorded".
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
|
|
|
18
18
|
|
|
19
19
|
- **The live app**: a site created here has none yet, so the before stays unset, which the report says (live-app-address).
|
|
20
20
|
- **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
|
|
21
|
-
- **A step fails before the test drive is pushed** (the sign-in
|
|
21
|
+
- **A step fails before the test drive is pushed** (the sign-in not completed, the site not created, the repository not connected): the Netlify setup ends there. Set the preview up as server-previews says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Netlify's GitHub app access to the repository", with the `access` page in the report. The sign-in or the GitHub app's access skipped is a no that ends the setup, as the playbook says.
|
|
22
22
|
- **No preview for the draft**: save no method, set up no other preview, and leave the draft as it is, unrecorded, as pull-requests' "No preview, no test drive" says; the site and the connection stay, and the report names them.
|
|
23
23
|
- **The build failed** (`state` is `error` or `failure`): say so under "What is not verified", with the command that builds this linked checkout as Netlify does and prints why, for the person to run: `netlify deploy --build --context deploy-preview`. It never runs here, since it builds with their Netlify sign-in.
|
|
24
24
|
- **No deploy preview within five minutes**: the wait's own screen opens the site's deploy settings, where Deploy Previews are turned on, and waits again on the person's yes (a site whose settings said they were off was asked about in step 3, so this wait is for a build, not a setting). When they skip it, or none came after its retries, say under "What is not verified" that Netlify built no deploy preview here, and that Deploy Previews may be off in the site's settings.
|
|
@@ -12,14 +12,15 @@ impeccable_prove_preview answered a `gate`: the host's own lock stands in front
|
|
|
12
12
|
- Any answer with `replaced` but `saved`: the run set a new Netlify site password, which replaced the one before, and it is not saved for reviews; the report says so, and that the person sets a password they know in Visitor access and in the app's Settings, whatever step comes next.
|
|
13
13
|
- `refused` with `protection: "none"` (Netlify: the site's settings show neither a password nor the team login, or the site is public now, `madePublic` saying who made it so, and the page past the login is not the app's), or on Vercel whose `reason` says the settings show no Deployment Protection: the page you proved is the app's own sign-in or another page, however it is worded, and the previews are open. Prove a page a visitor opens without signing in, as vercel-previews' or netlify-previews' step 1 says, and on `proven: true` go on with that case's step 2, the save, calling this tool no more: there is no lock to get past. No such page proven: the host's previews do not show this app to a visitor, so go on as that case's "Another app, or no answer" says (set the preview up as server-previews says, and say under "What is not verified" what the address answered), never step 3, whose lock the settings show is not there.
|
|
14
14
|
- Any other `refused` on Netlify whose `reason` says to prove or try a page a visitor opens, or that the page asks for the app's own sign-in past the lock: the site password (Netlify's own, or the one the run set) got past the lock to the app's own sign-in on the page you proved, or Netlify's form did not take it, or Netlify did not answer. Prove a page a visitor opens without signing in, as netlify-previews' step 1 says, and call the tool again; the consent is not asked a second time. Refused again, step 3.
|
|
15
|
-
-
|
|
15
|
+
- `skipped`: the person said no to giving access, a no that ends the setup, as the playbook says: nothing was saved, and the report says so under "What was skipped, and why", with the tool's `reason` and the settings page where the key is made (the report's bullet below), and `left` empty.
|
|
16
|
+
- Any other `refused`: nothing was saved; `reason` says what stopped it. Step 3.
|
|
16
17
|
- `save-failed`: the setup API did not take it all; `reason` says how far it got. Call the tool once more; when it fails again, step 3, with "Save the Vercel bypass secret on the setup page", "Type the Netlify site password in the app's Settings, under "A way past your host's lock"" or "Save the Cloudflare service token on the setup page" in `left`.
|
|
17
18
|
2. **Check it from Impeccable's side**: impeccable_write to `connection-check` with `{"appKey": "main"}` (on a repository with several apps, the app's own key: the tool saved the key for that app), and report what it answered. A check that answers `access: "auth-wall"` met the lock all the same: say so under "What is not verified", with its `detail`, and never as verified; the last screen does not call the app set up while it stands. Then go on with the case that sent you here: the proof stands, so save the method as vercel-previews', netlify-previews' or cloudflare-previews' step 2 says, Cloudflare's with the tool's `pattern`. Done when the method is saved.
|
|
18
19
|
3. **When nothing was saved**, save no method and set up no other preview, whatever the checkup's `next` says, and open no test drive (pull-requests' "No preview, no test drive"). Say under "What is not verified" that the previews are behind the host's lock, with the tool's `reason`, and name the settings page where the person makes the key, or makes the Netlify site public (the report's bullet below); put the person's one step in `left`, the settings action alone and never running the setup again (for Netlify's site password: "Type the Netlify site password in the app's Settings, under "A way past your host's lock"", and, with the draft test drive open, "Then mark draft #<n> ready", never a bypass key, which Netlify has none of; for Netlify's team login, none: the person was asked in this run, so `left` stays empty and the report names the Visitor access page); end the report's "Not part of this setup" line with the lock the tool met: "Vercel's Deployment Protection in front of the previews", "Netlify's password in front of the previews", "Netlify's team login in front of the previews", or "Cloudflare Access in front of the previews". Done when the report says so.
|
|
19
20
|
|
|
20
21
|
### Reference
|
|
21
22
|
|
|
22
|
-
- **Before the test drive's push, on Vercel** (pull-requests' step 4): impeccable_vercel_bypass with `beforePush: true`, and the app's `appKey` on a repository with several apps, reads the project's settings and, where Vercel's login will stand in front of the test drive's preview, asks the person now, on a screen naming the project and the app. `protected: false`, or `protected: true` with `consent` `given` or `not-needed`, or `protected: "unknown"`: push. Once the proof meets the gate, step 1 runs as written, and the yes is not asked again. `skipped`: push
|
|
23
|
+
- **Before the test drive's push, on Vercel** (pull-requests' step 4): impeccable_vercel_bypass with `beforePush: true`, and the app's `appKey` on a repository with several apps, reads the project's settings and, where Vercel's login will stand in front of the test drive's preview, asks the person now, on a screen naming the project and the app. `protected: false`, or `protected: true` with `consent` `given` or `not-needed`, or `protected: "unknown"`: push. Once the proof meets the gate, step 1 runs as written, and the yes is not asked again. `skipped`: push nothing: a no that ends the setup, as the playbook says, with the tool's `reason`.
|
|
23
24
|
- **The report** names, under "What was configured, and by whom", the Vercel project that holds the secret named "Impeccable reviews" (`project`), whether this run created it, found it there or the person typed it, and where it is revoked: the project's Settings → Deployment Protection → Protection Bypass for Automation; or the Netlify site (`site`) whose password is stored, whether this run read it from the site or set a new one on it, replacing the one before, and where it is changed: the site's Project configuration → General → Visitor access; or, with `opened`, the Netlify site made public, by this run on the person's yes or by the person on Netlify's page (`madePublic`), so anyone with its address opens it, and where it is made private again: the same Visitor access; or the service token named "Impeccable reviews", whether this run created it or the person typed it, the Access application whose policy allows it (`application`), when a created one runs out (`expires`), and where it is revoked: Zero Trust → Access controls → Service credentials → Service Tokens; with `apiToken`, the Cloudflare API token the person made for the run is theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until a new one is saved on the setup page.
|
|
24
25
|
- **The tool does the writes**: the key as Impeccable's credential for the lock, and the setting that sends it (Vercel's `x-vercel-protection-bypass` header, Netlify's password form, or Cloudflare's `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers), both through the setup API, so impeccable_write refuses those two. The state's `preview-protection` item words the same calls, and shows `keySaved` once they are done.
|
|
25
26
|
- **A key saved before** (the item shows `keySaved` and a `check` that `passed`): the lock was handled by an earlier run or the person. Run step 2's check and go on; call the tool only when that check fails. On a repository with several apps, the item's `keySaved` speaks for one app at most: an app whose own proof answered a `gate` gets step 1 with its `appKey`, whatever another app's key did, and its step 2 check is its own.
|
|
@@ -5,10 +5,10 @@ No host's previews show the pages: no host case applied, or one ended without a
|
|
|
5
5
|
### Steps
|
|
6
6
|
|
|
7
7
|
1. **Ask where the previews come from**, when the pages need a server from this repository (SSR, API routes, or a backend the front end calls; a front end whose bundle builds on its own still needs its backend here), once, with impeccable_ask: `id: "preview-host"`, `kind: "single"`, `title` *"Pull request previews"*, `prompt` *"Which host should deploy a preview of each pull request?"*, `hint` *"Impeccable reviews this app on its host's previews, and no longer runs its server itself."*, and `options` Vercel (`vercel`) and Netlify (`netlify`). Done when it answered a value, or a skip.
|
|
8
|
-
- **Several apps**: ask once for each app this case applies to, the title naming the app (*"Order desk previews"*).
|
|
9
|
-
- **Asked already for this app in this run**, or a host's setup for it ended without a preview (
|
|
8
|
+
- **Several apps**: ask once for each app this case applies to, the title naming the app (*"Order desk previews"*) and `appKey` its key, so a skip ends that app's setup alone.
|
|
9
|
+
- **Asked already for this app in this run**, or a host's setup for it ended without a preview (a step failed): ask nothing, and go on as step 3 says. A host's setup that ended on a no (the sign-in skipped) ended the setup for the app, as the playbook's "A no that ends the setup" says.
|
|
10
10
|
2. **Set the host up**: on `vercel`, as the vercel-setup case says, and on `netlify`, as the netlify-setup case says. The pick stands in for that case's "When this applies": its steps run whatever the checkout holds for the host, and even where the host already deploys previews that showed another app or did not answer, since the test drive's own preview, proven on that host, decides. Done when that case's steps are done.
|
|
11
|
-
3. **After a skip
|
|
11
|
+
3. **After a skip**, a no that ends the setup for the app, as the playbook says: under "What was skipped, and why", that the person skipped the host question, naming Vercel, Netlify and Cloudflare as the hosts whose pull request previews Impeccable reviews. **For pages that need no server**, or a host's setup that failed: no preview, no test drive. Save no method and write no workflow for these pages, and open no test drive, as pull-requests' "No preview, no test drive" says. Mark the Preview method and Test drive tasks failed, and say under "What was skipped, and why" that no host deploys previews of the pages (and that they need a server, where they do), naming Vercel, Netlify and Cloudflare as the hosts whose pull request previews Impeccable reviews. The checkup's entry for this preview then stays in `next`, which counts as done for the playbook's step 5. With several apps this holds for that app alone: the other chosen apps' previews and the test drive go on as monorepo says, and the tasks fail only where no app has a preview. Done when the report says so.
|
|
12
12
|
|
|
13
13
|
### Reference
|
|
14
14
|
|
|
@@ -4,17 +4,17 @@ The app has its own sign-in: a form that takes a username or an email and a pass
|
|
|
4
4
|
|
|
5
5
|
### Steps
|
|
6
6
|
|
|
7
|
-
1. **Find the roles** in the auth code (the login
|
|
7
|
+
1. **Find the roles** in the auth code (the login routes and pages, the user model, role checks and route guards), and use the names in `rolesDetected` for the ones it lists. A role is a kind of user that sees different pages; an app without roles has one, `default`. Each role signs in at its own login page where the app has one per role (`/admin`, `/manager`), else at the app's one login page. Then look for each role's username or email in the seed (`supabase/seed.sql`, `prisma/seed.ts`, `db/seeds.rb`), the fixtures (`fixtures/`, test fixtures), `.env.example` and the README, and note the file it came from. Done when every role has a name, a label, its login page, and its username with that file or none.
|
|
8
8
|
2. **Ask for the accounts**, back to back. Done when each role has a username and a password reference, or was skipped.
|
|
9
9
|
- **Every role saved already** (`rolesMissing` empty, `rolesSaved` not): ask nothing, not even the trust screen, and go to step 4.
|
|
10
|
-
- Otherwise first, once, the trust screen: `kind: "confirm"`, `title` *"
|
|
11
|
-
- Then one question per role: `kind: "account"`, `title` the role's label (*"Admin account"*), `
|
|
12
|
-
- **Some roles saved, some missing** (neither list empty): the write replaces the saved list as a whole, and the saved passwords never reach this run, so ask for every role, the saved ones first. The first account's hint: *"Saved accounts are typed again too, as the list is replaced whole; skip to add only the missing ones on the setup page."* A skip ends the questions, and the roles not asked yet count as skipped.
|
|
13
|
-
3. **Save them** with one impeccable_write to `test-accounts`: every answered role in the body `writes.testAccounts` gives, each `password` the reference its question returned. When `rolesSaved` is not empty and the person skipped any role, write nothing: the saved accounts stay as they are. Done when the answer names the saved roles, or nothing was written.
|
|
14
|
-
4. **Report** the saved roles under "What was configured, and by whom", and the skipped ones under "What was skipped, and why". Each role still without a saved account goes into `left` ("Add the staff test account on the setup page"). Done when every role is in the report once.
|
|
10
|
+
- Otherwise first, once, the trust screen: `kind: "confirm"`, `title` *"Your app has a sign-in"*, `hint` word for word two paragraphs with a blank line (`\n\n`) between them, *"Impeccable signs in as each kind of user to review the pages they see. It found 3: Admin, Manager and Customer."* with the count and the labels of the roles found (one role: *"Impeccable signs in with a test account to review the pages people see after signing in."*), then *"Passwords are stored encrypted and used only to sign in."*, `prompt` *"Add a test account for each?"* (one role: *"Add a test account?"*), `confirmLabel` *"Add accounts"*, `skipLabel` *"Skip for now"*. A skip ends the account questions: ask for no role, and every role counts as skipped.
|
|
11
|
+
- Then one question per role, in the order found: `kind: "account"`, `title` the role's label and its place, *"Admin account · 1 of 3"* (two spaces either side of the dot; one role: *"Admin account"*, or *"Test account"* for `default`), `hint` *"Signs in at /admin. Email found in supabase/seed.sql."*, or *"Signs in at /admin. No email found in the repository."* (*"Username found in"* for a username that is not an email), `prompt` *"Add the admin test account."*, and `prefill` the username or email found, when there is one. The screen shows the title, the hint and the two fields; the prompt is the web page's line. It answers the username and a password reference, or a skip, which skips that role only.
|
|
12
|
+
- **Some roles saved, some missing** (neither list empty): the write replaces the saved list as a whole, and the saved passwords never reach this run, so ask for every role, the saved ones first. The first account's hint ends: *"Saved accounts are typed again too, as the list is replaced whole; skip to add only the missing ones on the setup page."* A skip ends the questions, and the roles not asked yet count as skipped.
|
|
13
|
+
3. **Save them** with one impeccable_write to `test-accounts`: every answered role in the body `writes.testAccounts` gives, each `password` the reference its question returned and each `loginPage` that role's own. When `rolesSaved` is not empty and the person skipped any role, write nothing: the saved accounts stay as they are. Done when the answer names the saved roles, or nothing was written.
|
|
14
|
+
4. **Report** the saved roles under "What was configured, and by whom", and the skipped ones under "What was skipped, and why", each with its login page. Each role still without a saved account goes into `left` ("Add the staff test account on the setup page"). Done when every role is in the report once.
|
|
15
15
|
|
|
16
16
|
### Reference
|
|
17
17
|
|
|
18
18
|
- impeccable_ask and impeccable_write are the terminal prompt the instructions' `signIn` describes: the person types each password hidden, and it goes straight to `test-accounts`.
|
|
19
|
-
- **A password is only the reference impeccable_ask returned**; impeccable_write refuses any other, so a password in the seed, a README or an env file stays where it is. When the
|
|
20
|
-
- `loginPage` is the
|
|
19
|
+
- **A password is only the reference impeccable_ask returned**; impeccable_write refuses any other, so a password in the seed, the fixtures, a README or an env file stays where it is: never put one in a question, a prefill, a write or the report. When the repo sets one, end the account question's hint with *"Its password is in <file>."* and let the person type it.
|
|
20
|
+
- `loginPage` is the path of the page whose form signs that role in, such as `/admin`; every account carries its own, and reviews sign each role in there.
|
|
@@ -8,7 +8,7 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
|
|
|
8
8
|
- **`cli: "missing"`**: the CLI was not on this machine, and the person said Install on the tool's screen for it: install it with `npm install -g vercel@62.7.0`, exactly so (the run installs it with npm alone, pinned to that version, only after that Install, and refuses another package manager, version or tag); then ask again. The setup screen shows no second time; an install that did not complete brings the steps for the person's own install up instead.
|
|
9
9
|
- **`teams` in the answer instead of `team`** (several teams, none current): ask once which owns the project, `kind: "single"`, `id: "vercel-team"`, `options` the teams as given (the tool words the rest). The pick is the team.
|
|
10
10
|
- **Neither `team` nor `teams`**: the account has no team, and `vercel link --yes` without `--team` links in the person's own scope.
|
|
11
|
-
- **Skip**:
|
|
11
|
+
- **Skip**: a no that ends the setup, as the playbook says: say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
|
|
12
12
|
2. **Create the project, then link it** (run each Vercel command plain, on its own: its answer is short, a pipe to `tail` is refused, and a command chained after it leaves the project unrecorded): `vercel project add <the folder's name> --team <the team>` creates the project in that team, under the name `vercel link` gives a project (the checkout folder's name, lowercase); then `vercel link --yes --team <the team>`, in the checkout's root (never with `--cwd` or after a `cd`), links the folder to the project of that name, which is now the run's. The CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when the link prints the linked project.
|
|
13
13
|
- **`vercel project add` fails** (the name is in use: the project is the person's, not this run's): run no link; the Vercel setup ends there, and with the checkout linked to no project its CLI stops and the hold on pushes goes. Go on as "A step fails before the test drive is pushed" says, naming the project under "What was skipped, and why". When `.vercel/project.json` was in the checkout before the run, that link is not this run's, and the hold on pushes stays, by design: no test drive, as "No preview for the pushed test drive" says, and the report says under "What was skipped, and why" that the person removes `.vercel/project.json` or links the project themselves and runs the setup again.
|
|
14
14
|
3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, when that project is the one step 2 created or one connected to this repository already, asks the person for Vercel's GitHub app's access when the app cannot reach the repository, and then, on a project with no production deployment, puts Vercel's login in front of every address of the project, its own included. Vercel makes a new project's first deployment Production whatever branch it builds and serves it at the project's own address, and the test drive's push is that first deployment. When Vercel refuses the lock, the tool asks the person on one screen to turn it on in the project's settings and waits for it. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
|
|
@@ -19,11 +19,11 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
|
|
|
19
19
|
|
|
20
20
|
- **The live app**: a project created here has none yet, which the report says; one that deploys production already (`production: "exists"`) answers its address as `productionUrl`, and live-app-address says how it becomes the before.
|
|
21
21
|
- **The aim** holds here: the CLI does the work, and the person is asked only what the playbook's aim allows.
|
|
22
|
-
- **A step fails before the test drive is pushed** (the sign-in
|
|
22
|
+
- **A step fails before the test drive is pushed** (the sign-in not completed, the project not created, the repository not connected, or step 3 answering that the linked project is not this run's: `vercel link` linked one of the person's projects, named like the folder, which is left as it is): the Vercel setup ends there. Set the preview up as server-previews says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Vercel's GitHub app access to the repository", with the `access` page in the report. The sign-in, the team or the GitHub app's access skipped is a no that ends the setup, as the playbook says.
|
|
23
23
|
- **`pullRequestPreviews` in impeccable_hosted_previews' answer**: what vercel.json and the linked project say of the test drive's branch, read with the evidence and never changed by the run, since the setting is the repository's own or has no command (impeccable_enable_previews takes no Vercel). `enabled: false` (`git.deploymentEnabled` in vercel.json turns the branch off, or Vercel records no deployments on GitHub, where Impeccable finds its previews): the Vercel setup ends before step 4, as "A step fails before the test drive is pushed" says, and say in one line under "What was skipped, and why" that Vercel builds no preview of the pull requests, with the `reason`, and the exact change for the person: the vercel.json rule to lift, or Vercel GitHub deployments under the project's Settings → Git. `enabled: 'unknown'` (an Ignored Build Step decides commit by commit, or the CLI could not read the project): go on, and say so in one line under "What is not verified".
|
|
24
24
|
- **A push before step 3** is refused, since a connected project builds every push: run impeccable_vercel_connect first.
|
|
25
25
|
- **On a repository with several apps**, the project step 2 creates builds the repository's root: impeccable_vercel_connect connects and locks it only where an app this run sets up is the root's, and otherwise answers so, and the Vercel setup ends there for that app, as "A step fails before the test drive is pushed" says.
|
|
26
|
-
- **`locked: false`** from step 3, with `production` not "exists" (the person skipped the tool's screen, or the lock was still off after its retries): push nothing, and set up no other preview either, since the connected project would build that push as its first deployment, public at its own address. Mark the Test drive task failed, and say under "What was skipped, and why" what stopped the lock (the answer's `reason`).
|
|
26
|
+
- **`locked: false`** from step 3, with `production` not "exists" (the person skipped the tool's screen, or the lock was still off after its retries): push nothing, and set up no other preview either, since the connected project would build that push as its first deployment, public at its own address. Mark the Test drive task failed, and say under "What was skipped, and why" what stopped the lock (the answer's `reason`). The screen skipped is a no that ends the setup, as the playbook says.
|
|
27
27
|
- **No preview for the pushed test drive**: save no method, set up no other preview and open no pull request, as pull-requests' "No preview, no test drive" says; the project and the connection stay, and the report names them.
|
|
28
28
|
- **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed. When the cause is in the test drive's own change, correct it, commit, push once more and wait for that build; else the report names the cause.
|
|
29
29
|
- **No deployment within five minutes**: the wait's own screen opens the project's Git settings and waits again on the person's yes. When they skip it, or none came after its retries, say under "What is not verified" that Vercel built no push here, and that the project's Git connection is the thing to check.
|
package/dist/playbook.js
CHANGED
|
@@ -10,6 +10,18 @@ export function loadCases(directory = new URL('./playbook/cases/', import.meta.u
|
|
|
10
10
|
.sort()
|
|
11
11
|
.map((name) => ({ slug: name.slice(0, -3), content: readFileSync(join(path, name), 'utf8') }));
|
|
12
12
|
}
|
|
13
|
+
/** The test drive under --signed-in-test-drive, an eval's flag (#410): a page behind the app's own sign-in, which the review signs in to see. The fence and impeccable_test_pr hold the test drive to a saved account. */
|
|
14
|
+
const SIGNED_IN_TEST_DRIVE = `
|
|
15
|
+
|
|
16
|
+
# The signed-in test drive
|
|
17
|
+
|
|
18
|
+
This run was started with --signed-in-test-drive: the test drive changes a page only a signed-in visitor reaches, so Impeccable's review signs in with the saved test accounts to see it. It changes the pull-requests case as below, for each app's test drive on a repository with several.
|
|
19
|
+
|
|
20
|
+
- **Accounts first**: set up the sign-in as the sign-in case says before the test drive's push, even where a host's case goes to pull-requests' step 4 earlier. The fence pushes and opens a test drive only for an app with a test account saved.
|
|
21
|
+
- **No account saved** (the app has no sign-in of its own, or every account was skipped): the flag cannot be honoured. Push and open no test drive, on a public page or any other: mark the Test drive task failed and say why under "What was skipped, and why"; impeccable_report opens the report with the flag's line itself.
|
|
22
|
+
- **The page** (step 2): the new page sits behind the sign-in, guarded as the app guards its other signed-in pages, and is linked from a page every role reaches once signed in, such as the page they land on; a login page or a page a visitor opens signed out holds no link. Where no page reaches every role, link it from one role's page and name that role in the test drive's body.
|
|
23
|
+
- **The proof** (step 4): prove the preview on a page it shows a visitor signed out, the login page where the test drive's page sends that visitor to the sign-in.
|
|
24
|
+
- **The body** (step 5): it says the page is behind the sign-in, and which roles reach it.`;
|
|
13
25
|
/** The system prompt: the setup, its steps, then the reference and the cases. The instructions JSON carries every write shape and the fence decides what runs; this restates neither. */
|
|
14
26
|
export function playbook(repo) {
|
|
15
27
|
return `You are Impeccable's setup agent, in a checkout of ${repo.owner}/${repo.name} on the person's own machine. This setup covers one situation: Impeccable previews the repository's web front end, taking the previews its host deploys for pull requests (vercel-previews, netlify-previews, cloudflare-previews, or vercel-setup and netlify-setup, which turn them on) and signs in with the app's own username-or-email-and-password accounts when it has a sign-in. You read files and run commands here, save Impeccable's settings with the impeccable_* tools, and open pull requests as the person. The person watches this terminal, and a web page mirrors your tasks and the report.
|
|
@@ -41,8 +53,11 @@ The state names the setup page's other cards in \`notPartOfThisSetup\`. They are
|
|
|
41
53
|
- Ask through impeccable_ask, which shows a dialog and returns the answer; a question written in your message reaches nobody.
|
|
42
54
|
- Ask only for what the repository cannot tell you. Running this command is the person's go-ahead for every step, so ask no approval questions.
|
|
43
55
|
- Ask the questions that belong together back to back, one call each.
|
|
44
|
-
- Every question offers Skip: after a skip, carry on without the answer, and say in the report what it left undone.
|
|
45
|
-
- **No run ends by telling the person to run the setup again**: what a step needs from them is asked in this run, on the tool's screen, and the run carries on. After a skip, or retries that ran out, the report says what stopped it, and no \`left\` line or question asks for a rerun.
|
|
56
|
+
- Every question offers Skip but the apps question on a repository with several apps, which needs at least one app (the monorepo case): after a skip, carry on without the answer, and say in the report what it left undone, unless the skip is a no that ends the setup (below).
|
|
57
|
+
- **No run ends by telling the person to run the setup again**: what a step needs from them is asked in this run, on the tool's screen, and the run carries on. After a skip, or retries that ran out, the report says what stopped it, and no \`left\` line or question asks for a rerun. Only the command's own last screen, after a no that ended the setup, says when to run it again.
|
|
58
|
+
|
|
59
|
+
## A no that ends the setup
|
|
60
|
+
Impeccable reviews each pull request by opening its preview, so a no that leaves an app with no preview Impeccable can open **ends the setup for that app**. A no is the person's skip of a screen that sets previews up (a host's sign-in or its team, the access of a host's GitHub app to the repository, the lock on a new Vercel project, turning pull-request previews on, creating or connecting a project, the host question) or that gives access to protected ones (impeccable_vercel_bypass, impeccable_netlify_password or impeccable_cloudflare_access answering \`skipped\`). Then, for that app: no other preview, no push, no test drive, no method, and no more questions, its test accounts included (the playbook's steps 4 to 6 do not apply to it), which the tools refuse; mark its tasks left skipped, and say under "What was skipped, and why" which no ended it, with the tool's \`reason\`, and \`left\` empty. On a repository with several apps, the other apps carry on as usual. Once no app this run set up has a preview, call impeccable_report: the last screen says why the setup ended. A sign-in that did not complete, a build that never came or a refusal is no no, and goes as its case says; a skipped test account ends nothing, since the public pages can be reviewed.
|
|
46
61
|
|
|
47
62
|
## Commands
|
|
48
63
|
The fence runs the ordinary commands of a setup, never runs what could destroy data, and refuses everything else with the reason, without asking anyone. **Never ask the person to run a command**, in a question or anywhere else: after a refusal, take another way with what runs (a package script, a config change, another tool), and when there is none, leave the step undone and put it in the report under "What was skipped, and why", with the exact command for the person.
|
|
@@ -82,7 +97,7 @@ impeccable_report ends the run: no tool or model call works after it, so call it
|
|
|
82
97
|
|
|
83
98
|
Each case says when it applies; follow every case that does.
|
|
84
99
|
|
|
85
|
-
${loadCases().map(({ slug, content }) => `## Case: ${slug}\n\n${content}`).join('\n\n')}`;
|
|
100
|
+
${loadCases().map(({ slug, content }) => `## Case: ${slug}\n\n${content}`).join('\n\n')}${repo.signedInTestDrive ? SIGNED_IN_TEST_DRIVE : ''}`;
|
|
86
101
|
}
|
|
87
102
|
/** The first message the person sees in the terminal. */
|
|
88
103
|
export function firstMessage(repo) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The summary in each pull request's description, with the pages it changes before and after (#402): pristine's
|
|
3
|
+
* `modules.overview`, off by default because it writes into the author's description, so the person is asked once,
|
|
4
|
+
* worded here and never by the model. One question per repository, since pristine keeps the setting there.
|
|
5
|
+
*/
|
|
6
|
+
export const PR_SUMMARY_SCREEN = {
|
|
7
|
+
id: 'pr-summary',
|
|
8
|
+
kind: 'confirm',
|
|
9
|
+
title: 'See every change, before and after',
|
|
10
|
+
hint: 'Each pull request gets side-by-side screenshots of the pages it changes, right in its description, so reviewers spot the difference at a glance. Turn it off any time in Settings.',
|
|
11
|
+
prompt: 'Show before and after on your pull requests?',
|
|
12
|
+
confirmLabel: 'Show it',
|
|
13
|
+
skipLabel: 'Not now',
|
|
14
|
+
};
|
|
15
|
+
/** The end screen's line when the summary is not on. */
|
|
16
|
+
export const PR_SUMMARY_OFF = 'Before and after is off: turn it on any time in Settings.';
|
|
17
|
+
const modulesOf = (data) => {
|
|
18
|
+
const modules = data?.effective?.modules;
|
|
19
|
+
return modules && typeof modules === 'object' && !Array.isArray(modules) ? modules : null;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Reads the repository's review settings, and unless the summary is on already asks the person; on their yes, reads
|
|
23
|
+
* them again, since Settings may have changed while the question was up, and saves `modules.overview: true` with every
|
|
24
|
+
* other module as it is in force then: the save replaces the whole `modules`, and the API has no conditional write.
|
|
25
|
+
* A Not now, an answer that never came (headless, closed input) or a settings read that failed writes nothing.
|
|
26
|
+
*/
|
|
27
|
+
export async function askPrSummary(read, ask, save) {
|
|
28
|
+
const inForce = async () => {
|
|
29
|
+
const current = await read();
|
|
30
|
+
return current.ok ? modulesOf(current.data) : null;
|
|
31
|
+
};
|
|
32
|
+
const before = await inForce();
|
|
33
|
+
if (!before)
|
|
34
|
+
return 'off';
|
|
35
|
+
if (before.overview === true)
|
|
36
|
+
return 'on';
|
|
37
|
+
if ((await ask(PR_SUMMARY_SCREEN)).skipped)
|
|
38
|
+
return 'off';
|
|
39
|
+
const modules = await inForce();
|
|
40
|
+
if (!modules)
|
|
41
|
+
return 'off';
|
|
42
|
+
if (modules.overview === true)
|
|
43
|
+
return 'on';
|
|
44
|
+
return (await save({ modules: { ...modules, overview: true } })).ok ? 'on' : 'off';
|
|
45
|
+
}
|