@impeccable/reviews 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,6 @@ A test account's password, typed hidden on the account's screen (after a screen
84
84
 
85
85
  Vercel's bypass secret, which lets reviews open previews behind Vercel's login, is handled the same way. When Vercel's login stands in front of a preview, the command asks you once, on one screen, whether to create a bypass secret for Impeccable, and on your yes creates it as you through the Vercel CLI, labelled "Impeccable reviews" (`vercel api` for Vercel's protection-bypass route; an older CLI's `vercel project protection enable --protection-bypass`), then reads it back from the project's settings. A project that already holds a secret with that label gives it without a question. When the CLI is signed out, this folder is not linked, or your role cannot make one, a screen names the settings page and takes the secret you type, hidden. It opens the preview with the secret first and saves it, with the setting that sends it in the `x-vercel-protection-bypass` header, only once the page shows your app. The agent never holds it: the secret goes from the command's own process to Vercel and to Impeccable's setting, and the debug log and the transcript remove it. Vercel documents this as Protection Bypass for Automation, which every plan includes; you revoke it in the project's Settings, Deployment Protection.
86
86
 
87
- Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password" on a credit-based Pro plan) is handled the same way. When a Netlify deploy preview asks for it, the command reads the site's settings through the Netlify CLI, signed in as you. For a password Netlify gives back, it asks you once, on one screen, whether to store the site password for Impeccable, and on your yes reads it from the site as you. When Netlify does not give the password back, the CLI is signed out or the site is in none of your teams, a screen takes the password you type, hidden (the run never sets one itself: the Netlify CLI takes a new password only on its command line, where any process could read it). It opens the preview with the password first, as basic credentials in the `Authorization` header (Netlify's password page asks for a password alone, so the credentials carry no username), and saves it, with the setting that sends that header, only once the page shows your app; a password that opens the preview on Netlify's own form but not in the header is reported, not saved, since reviews can send only a header. A site behind Netlify's team login (a private site, or team login protection) has no way in for tools: the run reports it and takes no method. The agent never holds the password: it goes from the command's own process to Netlify and to Impeccable's setting, and the debug log and the transcript remove it.
87
+ Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password" on a credit-based Pro plan) is handled the same way. When a Netlify deploy preview asks for it, the command reads the site's settings through the Netlify CLI, signed in as you. For a password Netlify gives back, it asks you once, on one screen, whether to store the site password for Impeccable, and on your yes reads it from the site as you. When Netlify does not give the password back, the CLI is signed out or the site is in none of your teams, a screen takes the password you type, hidden (the run never sets one itself: the Netlify CLI takes a new password only on its command line, where any process could read it). It opens the preview with the password first, as basic credentials in the `Authorization` header (Netlify's password page asks for a password alone, so the credentials carry no username), and saves it, with the setting that sends that header, only once the page shows your app; a password that opens the preview on Netlify's own form but not in the header is reported, not saved, since reviews can send only a header. A site behind Netlify's team login (a private site) has no way in for tools, so the command asks you once, on one screen, whether to make the site public, and on your yes makes it public as you through the Netlify CLI (`netlify api updateSite` with `sso_login` off); anyone with its address can then open it, the live site included, until you make it private again in Project configuration, General, Visitor access. Where Netlify refuses (your team's default keeps every project private, or your role cannot change it), the command opens that page in your browser and asks you to try again once you changed it there; a password you set there instead is stored as above. The agent never holds the password: it goes from the command's own process to Netlify and to Impeccable's setting, and the debug log and the transcript remove it.
88
88
 
89
89
  A Cloudflare Access service token, which lets reviews open Pages previews behind Access, is handled the same way. When Access stands in front of a preview, the command asks you once, on one screen, whether to create a service token for Impeccable. On your yes it creates the token as you through Cloudflare's CLI, cf (run through npx), named "Impeccable reviews", and allows it in a Service Auth policy of the Access application in front of the preview (a reusable policy added to the application's list). When cf here is not signed in with Access, your browser opens Cloudflare's sign-in first, for five scopes (`account:read`, `user:read`, `pages:read`, `access.write`, `access-service-token.write`), with a retry when it did not complete. Only where that sign-in cannot create the token (it grants no Access, cf does not run here, or the application runs on policies of its own, which cf has no command for) does the command ask for a Cloudflare API token with two permissions (Access: Apps and Policies, Edit; Access: Service Tokens, Edit), used for that run only and stored nowhere. On a skip, or where that cannot be done, a screen takes a client id and secret you paste, the secret hidden. It opens the preview with the token in the `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers first and saves it, with the setting that sends them, only once the page shows your app; a token it made that opens nothing is deleted, with its policy. The agent never holds either, and the debug log and the transcript remove both. You revoke it in Zero Trust, Access controls, Service credentials, Service Tokens.
package/dist/fence.js CHANGED
@@ -330,7 +330,7 @@ function scriptsIn(dir) {
330
330
  * package.json and the files the scripts and listed tools load from the checkout by convention (`*.config.js`, `.*rc`): once the agent
331
331
  * changes one, what runs is no longer the repository's own code.
332
332
  */
333
- /** The ci-tunnel case corrects Vite's preview.allowedHosts and proves it with the local preview check, which runs in its own copy and environment: after that edit only the shell stops. */
333
+ /** A rerun on Impeccable's tunnel (first-preview-failure) corrects Vite's preview.allowedHosts and proves it with the local preview check, which runs in its own copy and environment: after that edit only the shell stops. */
334
334
  const VITE_CONFIG = /(?:^|\/)vite\.config\.[cm]?[jt]s$/i;
335
335
  const LOADED = /(?:^|\/)(?:package\.json|[\w.-]+\.config\.[cm]?[jt]s|\.[\w-]+rc(?:\.\w+)*|tsconfig[^/]*\.json|knexfile\.[^/]+|bunfig\.toml|prisma\/seed\.[^/]+|node_modules\/.+)$/i;
336
336
  /** Why scripts and tools stop running: the agent changed a package.json (true when it does not say which), or a file named here that they load. */
@@ -1183,6 +1183,10 @@ export function chooseHostTeam(log, questionId, team) {
1183
1183
  if (host && Object.hasOwn(HOST_CLIS, host) && hostsAgreed(log).has(host) && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(team))
1184
1184
  teamsChosen(log).set(host, team);
1185
1185
  }
1186
+ /** The team picked or found for a host's CLI in this run, which its dashboard's addresses name. */
1187
+ export function chosenHostTeam(log, host) {
1188
+ return teamsChosen(log).get(host);
1189
+ }
1186
1190
  /** What impeccable_vercel_connect settled of the linked Vercel project's first deployment in this run, by the run's log. */
1187
1191
  const settledVercel = new WeakMap();
1188
1192
  export function settleVercelFirstDeployment(log, settlement) {
@@ -21,7 +21,20 @@ export const HOST_QUESTIONS = {
21
21
  };
22
22
  /** Where a person reads or changes a site's password in Netlify, as Netlify's docs name the page on every plan. */
23
23
  export const NETLIFY_PASSWORD_WHERE = 'Project configuration → General → Visitor access';
24
- /** The screens a tool shows on its own: a sign-in's retry, the one-time access the host's GitHub app needs, and the consent to a key for a host's lock (a bypass secret for Vercel's login, the site password for Netlify's, a service token for Cloudflare Access), with the paste where the run does not get one. */
24
+ /** The wait for a host's build that ran out: the page where the host's builds of pull requests are turned on, and the wait again. */
25
+ const buildWait = (id, name, where) => ({
26
+ [`${id}-build`]: {
27
+ id: `${id}-build`,
28
+ kind: 'confirm',
29
+ title: `Waiting for ${name}`,
30
+ hint: `Five minutes and no preview yet. If ${name} hasn't started a build, ${where} on the page that opens. The run carries on by itself once the preview is built.`,
31
+ prompt: `Open ${name} and keep waiting?`,
32
+ confirmLabel: 'Keep waiting',
33
+ skipLabel: 'Skip',
34
+ },
35
+ [`${id}-build-retry`]: { id: `${id}-build-retry`, kind: 'confirm', title: `Waiting for ${name}`, prompt: `Still no preview from ${name}.`, confirmLabel: 'Keep waiting', skipLabel: 'Skip' },
36
+ });
37
+ /** The screens a tool shows on its own: a sign-in's retry, the one-time access the host's GitHub app needs, and the consent to a key for a host's lock (a bypass secret for Vercel's login, the site password for Netlify's, a service token for Cloudflare Access), with the paste where the run does not get one, and to making a site behind Netlify's team login public. */
25
38
  export const HOST_SCREENS = {
26
39
  'netlify-signin-retry': { id: 'netlify-signin-retry', kind: 'confirm', title: 'Sign in to Netlify', prompt: 'The sign-in did not complete.', confirmLabel: 'Try again', skipLabel: 'Skip' },
27
40
  'netlify-access': {
@@ -45,6 +58,20 @@ export const HOST_SCREENS = {
45
58
  skipLabel: 'Skip',
46
59
  },
47
60
  'vercel-access-retry': { id: 'vercel-access-retry', kind: 'confirm', title: 'Vercel needs access on GitHub', prompt: "Vercel still can't see the repository.", confirmLabel: 'Try again', skipLabel: 'Skip' },
61
+ // Vercel refused the run's lock on a new project: the person turns it on, since the test drive's push is the project's first deployment.
62
+ 'vercel-lock': {
63
+ id: 'vercel-lock',
64
+ kind: 'confirm',
65
+ title: 'Lock the new Vercel project',
66
+ hint: "The test drive's push is the project's first deployment, which Vercel shows to anyone at the project's own address. Vercel didn't let the run put its login in front. Turn on Vercel Authentication for All Deployments on the page that opens. The run carries on by itself once it's on.",
67
+ prompt: 'Open Vercel to lock the project?',
68
+ confirmLabel: 'Open Vercel',
69
+ skipLabel: 'Skip',
70
+ },
71
+ 'vercel-lock-retry': { id: 'vercel-lock-retry', kind: 'confirm', title: 'Lock the new Vercel project', prompt: "The project isn't locked yet.", confirmLabel: 'Try again', skipLabel: 'Skip' },
72
+ ...buildWait('vercel', 'Vercel', 'connect the repository in Settings → Git'),
73
+ ...buildWait('netlify', 'Netlify', 'turn Deploy Previews on'),
74
+ ...buildWait('cloudflare', 'Cloudflare', 'connect the repository to the Pages project'),
48
75
  // Vercel's login in front of the previews: the one consent to a bypass secret the run creates as the person, and the paste when the CLI cannot.
49
76
  'vercel-bypass': {
50
77
  id: 'vercel-bypass',
@@ -81,6 +108,25 @@ export const HOST_SCREENS = {
81
108
  prompt: `Netlify keeps your previews behind the site password, and the run could not read it: the Netlify CLI here is signed out, the site is in none of your teams, or Netlify does not give the password back. Find it, or set a new one, in ${NETLIFY_PASSWORD_WHERE}. It is stored encrypted and used only to open previews.`,
82
109
  hint: 'Paste the password',
83
110
  },
111
+ // Netlify's team login in front of the previews: the one consent to making the site public as the person, and the retry once they changed it on Netlify's page where Netlify refused.
112
+ 'netlify-visibility': {
113
+ id: 'netlify-visibility',
114
+ kind: 'confirm',
115
+ title: 'Your Netlify previews are private',
116
+ hint: `Impeccable reviews each pull request on its preview, and Netlify keeps this site behind your team's login, which tools can't get past. The run makes the site public as you, so anyone with an address of it can open it, the live site too. Change it back any time in ${NETLIFY_PASSWORD_WHERE}.`,
117
+ prompt: 'Make the site public?',
118
+ confirmLabel: 'Make public',
119
+ skipLabel: 'Skip',
120
+ },
121
+ 'netlify-visibility-retry': {
122
+ id: 'netlify-visibility-retry',
123
+ kind: 'confirm',
124
+ title: 'Your Netlify previews are private',
125
+ hint: "Netlify still shows its login in front of the previews. On the Netlify page that opened, set Visitor access to Public (or Password protected, on Pro). Public is greyed out until the site has a live deploy, or while your team's default keeps it private (Team settings → General → Visitor access).",
126
+ prompt: 'Changed it?',
127
+ confirmLabel: 'Try again',
128
+ skipLabel: 'Skip',
129
+ },
84
130
  // Cloudflare Access in front of the previews: the one consent to a service token the run creates as the person through cf, the retry of cf's sign-in, the API token where that sign-in cannot create it, and the paste where the run does not create one.
85
131
  'cloudflare-access': {
86
132
  id: 'cloudflare-access',
@@ -7,7 +7,8 @@
7
7
  * no password goes back to the model. The person consents once, on the tool's screen, before the password is stored;
8
8
  * a password opens the locked address before anything saves it. A password Netlify does not give back is the
9
9
  * person's to type: setting one through the CLI would put it on the command line, where any process could read it.
10
- * Netlify's team login (a private site, or team login protection) has no way in for tools, and is reported as such.
10
+ * Netlify's team login (a private site) has no way in for tools, so on the person's yes the run makes the site public
11
+ * through the CLI; where Netlify refuses, the person changes Visitor access on Netlify's page and the run tries again.
11
12
  */
12
13
  /** The header reviews send the password in. */
13
14
  export const PASSWORD_HEADER = 'Authorization';
@@ -15,6 +16,13 @@ export const PASSWORD_HEADER = 'Authorization';
15
16
  export const basicCredentials = (password) => `Basic ${Buffer.from(`:${password}`).toString('base64')}`;
16
17
  /** The CLI's read of a site by the address every deploy preview of it carries (`site_id` takes the site's default domain), an authenticated call of Netlify's API. */
17
18
  export const readSiteCall = (site) => ['api', 'getSite', '--data', JSON.stringify({ site_id: `${site}.netlify.app` })];
19
+ /**
20
+ * The CLI's change of a site's visitor access to Public: the team login off. `updateSite` documents no such field, but
21
+ * `getSite` reads it back and Netlify's own tools send it; `sso_login_context` stays out, since Netlify answers 422 with it.
22
+ */
23
+ export const publicSiteCall = (id) => ['api', 'updateSite', '--data', JSON.stringify({ site_id: id, body: { sso_login: false } })];
24
+ /** Netlify's page where a site's Visitor access is changed. */
25
+ export const visitorAccessPage = (site) => `https://app.netlify.com/projects/${site}/configuration/general#visitor-access`;
18
26
  const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
19
27
  /** A site's name and id as Netlify makes them. */
20
28
  const NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -38,11 +46,18 @@ export async function readSite(run, site) {
38
46
  /**
39
47
  * Gets the site password in place for the address Netlify's lock stands in front of, and saves only one that opens it.
40
48
  * The site's settings say what the lock is: a password, which the person's yes has the run store when Netlify gives it
41
- * back; the team login, which no tool gets past; or neither. When the CLI cannot read the site, or Netlify gives no
42
- * password back, the person may paste the password.
49
+ * back; the team login, which the person's yes has the run lift by making the site public; or neither. When the CLI
50
+ * cannot read the site, or Netlify gives no password back, the person may paste the password.
43
51
  */
44
52
  export async function passNetlifyLock(deps) {
45
- const site = await readSite(deps.run, deps.site);
53
+ let site = await readSite(deps.run, deps.site);
54
+ if (site?.teamLogin && !site.hasPassword) {
55
+ const opened = await openToVisitors(deps, site);
56
+ if ('status' in opened)
57
+ return opened;
58
+ // The person picked Password protected on Netlify's page: the password is got in place as any site password is.
59
+ site = opened;
60
+ }
46
61
  const from = site ? { site: site.name } : {};
47
62
  const finish = async (password, how) => {
48
63
  deps.remember(password);
@@ -73,9 +88,6 @@ export async function passNetlifyLock(deps) {
73
88
  return { status: 'refused', reason: done.locked ? "Netlify's own site password opened the address neither in the header nor on Netlify's form: the page asks for the app's own sign-in past the lock, or Netlify did not answer. Prove a page a visitor opens without signing in, and call this tool again." : done.reason, protection: 'password', ...from };
74
89
  }
75
90
  }
76
- else if (site.teamLogin) {
77
- return { status: 'login', protection: 'team-login', reason: "Netlify's team login stands in front of the previews: they open for a member of the Netlify team signed in in their browser, and Netlify gives tools no way in.", ...from };
78
- }
79
91
  else {
80
92
  return { status: 'refused', protection: 'none', reason: "Netlify's settings show neither a password nor the team login on the site: the sign-in is the app's own, so prove a page a visitor opens without signing in.", ...from };
81
93
  }
@@ -87,3 +99,46 @@ export async function passNetlifyLock(deps) {
87
99
  const done = await finish(typed, 'typed');
88
100
  return 'status' in done ? done : { status: 'refused', reason: done.locked ? `The password typed did not open the address: it is not the site's password, or the page asks for the app's own sign-in past the lock. ${done.reason}` : done.reason, ...from };
89
101
  }
102
+ /**
103
+ * A site behind Netlify's team login made public on the person's yes: the CLI's change, read back, then the preview
104
+ * proven. Where Netlify refuses (the team's default keeps every project private, the field is not taken, the role
105
+ * cannot) or the preview still asks for the login, its Visitor access page opens and the person tries again, twice at
106
+ * most. A password the person set there instead comes back as the site, for the password's own steps.
107
+ */
108
+ async function openToVisitors(deps, site) {
109
+ const from = { site: site.name, protection: 'team-login' };
110
+ if (!(await deps.makePublic()))
111
+ return { status: 'skipped', reason: 'The person skipped making the site public.', ...from };
112
+ let read = site;
113
+ /** The run's change went through: the site stays public whatever comes after, and every later answer says so. */
114
+ let ranPublic = false;
115
+ const kept = () => (ranPublic ? { madePublic: 'run' } : {});
116
+ for (let tries = 0;; tries++) {
117
+ if (read?.teamLogin && !read.hasPassword) {
118
+ try {
119
+ await deps.run(publicSiteCall(read.id));
120
+ read = await readSite(deps.run, deps.site);
121
+ if (read && !read.teamLogin)
122
+ ranPublic = true;
123
+ }
124
+ catch {
125
+ // Refused: the site as read stays, and the person's page is next.
126
+ }
127
+ }
128
+ if (read?.hasPassword)
129
+ return read;
130
+ if (read && !read.teamLogin) {
131
+ const proof = await deps.prove(undefined, false);
132
+ if (proof.proven)
133
+ return { status: 'opened', madePublic: ranPublic ? 'run' : 'person', ...from };
134
+ // Past the login, a page that is not the app's: the proof says why.
135
+ if (proof.gate === undefined)
136
+ return { status: 'refused', reason: proof.reason ?? 'The address did not open.', site: site.name, protection: 'none', ...kept() };
137
+ }
138
+ if (tries === 2)
139
+ return { status: 'login', reason: "Netlify kept its team login in front of the previews.", ...from, ...kept() };
140
+ if (!(await deps.retry(visitorAccessPage(site.name), tries === 0)))
141
+ return { status: 'skipped', reason: "The person skipped changing Visitor access on Netlify's page.", ...from, ...kept() };
142
+ read = await readSite(deps.run, deps.site);
143
+ }
144
+ }
@@ -10,8 +10,8 @@ Cloudflare Pages deploys previews of the pull requests: impeccable_cloudflare_pr
10
10
  ### Reference
11
11
 
12
12
  - **Behind Cloudflare Access** (`gate: "cloudflare-access"`): Cloudflare's login stands in front of the previews. Get past it as preview-protection says, which proves the preview, and go on with step 2; when nothing got past it, that case says how the run ends.
13
- - **Another app, or no answer from any of the three**: Cloudflare's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Cloudflare address answered.
14
- - **Nothing listed**: Cloudflare's previews are not on record, so set the preview up as ci-tunnel or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
13
+ - **Another app, or no answer from any of the three**: Cloudflare's previews do not show this app. Set the preview up as server-previews or static-site says instead, and say under "What is not verified" what the Cloudflare address answered.
14
+ - **Nothing listed**: Cloudflare's previews are not on record, so set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
15
15
  - **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.
16
16
  - The evidence is read only: link, deploy and change nothing on Cloudflare. The one Cloudflare sign-in is impeccable_cloudflare_access's, behind Access, on the person's yes. Cloudflare does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern, the branch's alias: save the pattern the proof answered, and no other.
17
17
  - The report names the address that answered with this app under "What we found", and the pattern saved under "What was configured, and by whom". The test drive's own preview, which Cloudflare builds after the run, goes under "What is not verified".
@@ -25,4 +25,4 @@ A rerun: the setup pull request an earlier run opened has a preview job that fai
25
25
  | upload | The real build output and its entry page; correct `outputDir`. |
26
26
  | seed | The migration or seed command and the throwaway database. |
27
27
  | start | `startCommand`, `port`, `env` and readiness. |
28
- | tunnel | The app refusing the forwarded Host (see ci-tunnel), not Impeccable's own access gate: a 403 on the bare link alone is not a broken preview. |
28
+ | tunnel | A repository already on Impeccable's tunnel: the app refusing the forwarded Host, allowed in the framework's host check for previews only (Rails `config.hosts`, Django `ALLOWED_HOSTS`, Vite `preview.allowedHosts`), not Impeccable's own access gate: a 403 on the bare link alone is not a broken preview. |
@@ -8,9 +8,10 @@ The state's repository has an `apps` item: the scan proposed several apps, folde
8
8
  - **An item that is `done` or `skipped`** was answered before, on the setup page or by an earlier run (a run that stopped at the question left the scan's default, as a skip does): write nothing and ask nothing; the state's apps that are `enabled` are the chosen ones. Step 4.
9
9
  2. **Ask which apps get previews**, once, with impeccable_ask: `id: "apps"`, `kind: "multi"`, `title` *"This repository has several apps"*, `prompt` *"Which apps should get Impeccable previews?"*, `hint` *"Each draws pages of its own; untick any that should not get previews."*, and `options` one per app the answer listed: `value` its `key`, `label` its `name` and folder (*"Storefront · apps/storefront"*), `selected: true` on every one. The repository's own app (`main`) is among them only where the Worker kept it as a product of its own, with pages outside every app's folder: its label is its name and `/`. Done when it answered `values`, or a skip.
10
10
  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 Skip**, write nothing more: the app the scan switched on stays the one that is on. Done when the last answer's `apps` show `enabled` true for every chosen app and false for the rest, or, after Skip, the confirm's answer stands.
11
- 4. **Read the state again** with impeccable_state. Done when the preview item's `apps` names each chosen app's `directory`, `build` and `method`, and `perApp` its status.
12
- 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 (ci-tunnel when its pages need a server, static-site when they do not, a host's case when the host deploys that app as a project of its own, proven on that app's own address), 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"*). Done when every chosen app's method is saved and its sign-in saved or skipped, and no other app got a write, a build or a question.
13
- 6. **The pull requests** as pull-requests says, once for the repository: the setup commit takes the state's one workflow `file` (a job for each chosen app whose preview Impeccable builds) once every chosen app's method and build settings are saved; the test drive adds its page to every chosen app, in that app's own framework and linked from that app's own public page, so one review renders each of them on its own preview; that case's step 3 proves each chosen app with the input that proved it; its step 6 checks once, with the first chosen app's `appKey`, and the other apps' previews go under "What is not verified". Done when pull-requests' steps are done.
11
+ 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.
12
+ 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 its pages need a server, static-site when they do not, a host's case when the host deploys that app as a project of its own, proven on that app's own address), 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"*). 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.
13
+ 6. **The pull requests** as pull-requests says, once for the repository: the setup commit takes the state's one workflow `file` (a job for each chosen static site whose preview Impeccable builds) once every chosen app's method and build settings are saved, or its preview skipped as server-previews says; the test drive adds its page to every chosen app with a preview (an app whose preview was skipped gets none, and goes under "What was skipped, and why"), in that app's own framework and linked from that app's own public page, so one review renders each of them on its own preview; that case's step 3 proves each chosen app with the input that proved it; its step 6 checks once, with the `appKey` of the first chosen app with a preview, and the other apps' previews go under "What is not verified". Done when pull-requests' steps are done.
14
+ - **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
15
  - **A host previews some chosen apps and Impeccable builds the others**: a host's case speaks for its own app. The setup commit carries the workflow for the others and the test drive branches from it, and pull-requests' step 4 goes each app's way: the host's app proven on the host's build, the others by the workflow. The repository goes without a workflow and a setup pull request only where a host previews every chosen app.
15
16
  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, the scan's default after a skip), its method, its sign-in, what was verified; an app left out is under "What was skipped, and why" as well, with the setup page as where it is turned on. Done when every app is on one line.
16
17
 
@@ -9,9 +9,9 @@ Netlify deploys previews of the pull requests: impeccable_netlify_previews lists
9
9
 
10
10
  ### Reference
11
11
 
12
- - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the site password, or Netlify's team login, stands in front of the previews. That is the preview-protection case's, whose steps come before step 2: the password is got past on the person's yes, the team login is not.
13
- - **Another app, or no answer from any of the three**: Netlify's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Netlify address answered.
14
- - **Nothing listed**: Netlify's previews are not on record. Where the checkout is set up for Netlify that is the netlify-setup case's; else set the preview up as ci-tunnel or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
12
+ - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the site password, or Netlify's team login, stands in front of the previews. That is the preview-protection case's, whose steps come before step 2: on the person's yes the password is stored, or the site behind the team login made public.
13
+ - **Another app, or no answer from any of the three**: Netlify's previews do not show this app. Set the preview up as server-previews or static-site says instead, and say under "What is not verified" what the Netlify address answered.
14
+ - **Nothing listed**: Netlify's previews are not on record. Where the checkout is set up for Netlify that is the netlify-setup case's; else set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
15
15
  - **The pull requests**: no setup pull request; the test drive branches from the default branch. Netlify builds a deploy preview only for a pull request, so the test drive opens as a draft, its deploy preview is waited for and the test drive's page proven on it, and then the draft is marked ready: pull-requests' step 4. In place of pull-requests' step 3, build the test drive branch once with the build Netlify runs (`netlify.toml`'s `[build]` `command`, else the package's `build` script), so a page that does not build is never pushed.
16
- - The evidence is read only: link, deploy and change nothing on Netlify. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
16
+ - The evidence is read only: link, deploy and change nothing on Netlify, beyond what impeccable_netlify_password does on the person's yes. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
17
17
  - The report names the address that answered with this app under "What we found", and the pattern saved under "What was configured, and by whom". The test drive's own preview, which Netlify builds after the run, goes under "What is not verified".
@@ -1,13 +1,13 @@
1
1
  ### When this applies
2
2
 
3
- The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a package script that runs the `netlify` command (a Netlify package alone, `@netlify/*` or `netlify-cli`, says nothing of where the repository deploys). And no pull request has a Netlify deploy preview: impeccable_netlify_previews lists none, and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in netlify-previews, the repository needs no workflow and no setup pull request.
3
+ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a package script that runs the `netlify` command (a Netlify package alone, `@netlify/*` or `netlify-cli`, says nothing of where the repository deploys); or the person picked Netlify in server-previews. And no pull request has a Netlify deploy preview: impeccable_netlify_previews lists none, and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in netlify-previews, the repository needs no workflow and no setup pull request.
4
4
 
5
5
  ### Steps
6
6
 
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 sign-in screen only when the Netlify CLI is signed out (a person signed in already is asked nothing), runs the login in the browser and offers a retry. Done when it answered `confirmed`.
8
8
  - **`cli: "missing"`**: install the CLI with the person's package manager (`npm install -g netlify-cli`, `pnpm add -g netlify-cli`, `yarn global add netlify-cli` or `bun add -g netlify-cli`), then ask again: the screen shows no second time.
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**: set the preview up as ci-tunnel or static-site says, and say under "What was skipped, and why" that Netlify was found and the sign-in was skipped. The steps below do not apply.
10
+ - **Skip**: set the preview up as server-previews or static-site says, and 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**: `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
  3. **Connect the repository** with impeccable_netlify_connect: it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, 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.
13
13
  4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes netlify-previews' reference gives: from the default branch, no setup pull request, built once with the build Netlify runs. Stage its files 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` succeeded.
@@ -15,10 +15,10 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
15
15
 
16
16
  ### Reference
17
17
 
18
- - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the site not created, the repository not connected): the Netlify setup ends there. Set the preview up as ci-tunnel or static-site 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.
18
+ - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the site not created, the repository not connected): the Netlify setup ends there. Set the preview up as server-previews or static-site 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.
19
19
  - **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.
20
20
  - **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.
21
- - **No deploy preview within five minutes**: the build had not finished, or Netlify builds no deploy preview for pull requests here: Deploy Previews may be off in the site's settings, so put "Turn Deploy Previews on in the site's Netlify settings" in `left`.
22
- - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the preview-protection case's, whose steps come before the save in step 5. A team that keeps new sites private puts Netlify's team login in front of the new site's previews, which no tool gets past: put "Make the site public, or password-protected, in Project configuration → General → Visitor access" in `left`.
21
+ - **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. 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.
22
+ - **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the preview-protection case's, whose steps come before the save in step 5. A team that keeps new sites private puts Netlify's team login in front of the new site's previews: the tool makes the site public on the person's yes, and step 5 goes on once it answers `opened`.
23
23
  - **The report** names the Netlify site under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Netlify account, with how the repository was connected and the CLI's install when there was one. The site's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
24
24
  - **Status lines**: the sign-in, the link and the connection belong to the Preview method task; from the page on, the Test drive task, so name the test drive in each line about its build, its push, the draft, the wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Netlify's login as a login, since a line about signing in belongs to the Sign-in task.
@@ -1,21 +1,23 @@
1
1
  ### When this applies
2
2
 
3
- impeccable_prove_preview answered a `gate`: the host's own lock stands in front of a preview the host made, and a visitor gets its login or its password page. Three locks have a way in for a tool, each with a tool of its own that works as the person in the command's own process, so no key ever reaches you: Vercel's Deployment Protection (`gate: "vercel-protection"`: Vercel Authentication, or Password Protection, which shows the same gate), whose way in is a bypass secret of the project, sent in a header, which every plan has; Netlify's site password (`gate: "netlify-protection"`), sent as basic credentials in the Authorization header; and Cloudflare Access in front of a Pages preview (`gate: "cloudflare-access"`), whose way in is a service token that a Service Auth policy of the Access application allows, sent in two headers. Netlify's team login, which the same gate answers for, has none. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well, and so does netlify-setup's new site on a team that keeps new sites private: these steps come between the proof and the save of the method, in vercel-previews', netlify-previews' or cloudflare-previews' step 2 or vercel-setup's or netlify-setup's step 5, before the test drive's pull request opens (on Netlify, before its draft is marked ready), and end with the preview opened.
3
+ impeccable_prove_preview answered a `gate`: the host's own lock stands in front of a preview the host made, and a visitor gets its login or its password page. Three locks have a way in for a tool, each with a tool of its own that works as the person in the command's own process, so no key ever reaches you: Vercel's Deployment Protection (`gate: "vercel-protection"`: Vercel Authentication, or Password Protection, which shows the same gate), whose way in is a bypass secret of the project, sent in a header, which every plan has; Netlify's site password (`gate: "netlify-protection"`), sent as basic credentials in the Authorization header; and Cloudflare Access in front of a Pages preview (`gate: "cloudflare-access"`), whose way in is a service token that a Service Auth policy of the Access application allows, sent in two headers. Netlify's team login, which the same gate answers for, has none, so the tool makes the site public on the person's yes. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well, and so does netlify-setup's new site on a team that keeps new sites private: these steps come between the proof and the save of the method, in vercel-previews', netlify-previews' or cloudflare-previews' step 2 or vercel-setup's or netlify-setup's step 5, before the test drive's pull request opens (on Netlify, before its draft is marked ready), and end with the preview opened.
4
4
 
5
5
  ### Steps
6
6
 
7
- 1. **Get the key in place** (the Preview method task): impeccable_vercel_bypass for Vercel's login, under the status line *"Getting past Vercel's login"*; impeccable_netlify_password for Netlify's lock, under *"Getting past Netlify's password"*; impeccable_cloudflare_access for Cloudflare Access, under *"Getting past Cloudflare Access"*. The tool does the rest: it finds the project's secret labelled "Impeccable reviews", or reads what Netlify's settings say stands in front of the site; shows one screen asking to create the secret, to store the site password Netlify gives back, or to create a service token (then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one; opens the preview with it; and saves it for reviews. The key never comes back to you. Done when it answered `status`:
7
+ 1. **Get the key in place** (the Preview method task): impeccable_vercel_bypass for Vercel's login, under the status line *"Getting past Vercel's login"*; impeccable_netlify_password for Netlify's lock, under *"Getting past Netlify's password"*; impeccable_cloudflare_access for Cloudflare Access, under *"Getting past Cloudflare Access"*. The tool does the rest: it finds the project's secret labelled "Impeccable reviews", or reads what Netlify's settings say stands in front of the site; shows one screen asking to create the secret, to store the site password Netlify gives back, to make a site behind Netlify's team login public (where Netlify refuses, it opens Netlify's Visitor access page and asks the person to try again once they changed it there), or to create a service token (then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one (Vercel's with the page where it is made opened); opens the preview with it; and saves it for reviews. The key never comes back to you. Done when it answered `status`:
8
8
  - `saved`: the preview is proven (`proven: true`); `secret`, `password` or `token` says whether it was created, existing or typed, and `project`, `site` or `application` which one holds or allows it; Cloudflare's `pattern` is the address pattern to save with the method, and `apiToken` says a Cloudflare API token the person made created the token. Step 2.
9
- - `login` (Netlify): the site is behind Netlify's team login, which no tool gets past; `reason` says so. Step 3.
9
+ - `opened` (Netlify): the site is public now and the preview proven (`proven: true`); `madePublic` says whether the run or the person made it public, and `site` which site. Step 2.
10
+ - `login` (Netlify): Netlify kept its team login in front through the retries; `reason` says so. Step 3.
11
+ - Any answer with `madePublic` but `opened`: the run made the Netlify site public and it stays so, though the preview did not open; the report names that change as the bullet below says, whatever step comes next.
10
12
  - `refused` whose `reason` says to prove a page a visitor opens: Netlify's own site password opened neither the header nor Netlify's form, so the page you proved asks for the app's own sign-in past the lock, 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.
11
13
  - Any other `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
12
14
  - `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", "Save the Netlify site password on the setup page" or "Save the Cloudflare service token on the setup page" in `left`.
13
15
  2. **Check it from Impeccable's side**: impeccable_write to `connection-check` with `{"appKey": "main"}`, and report what it answered. 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.
14
- 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 (the report's bullet below); 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.
16
+ 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 team login, "Make the Netlify site public in Visitor access", unless the answer has `madePublic`: the site is public already, so the step comes from the tool's `reason`); 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.
15
17
 
16
18
  ### Reference
17
19
 
18
- - **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 the person typed it, and where it is changed: the site's Project configuration → General → 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 this setup is run again.
20
+ - **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 the person typed it, 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.
19
21
  - **The tool does the writes**: the key as Impeccable's credential for the lock, and the setting that sends it in the host's header (`x-vercel-protection-bypass`, `Authorization`, or `CF-Access-Client-Id` and `CF-Access-Client-Secret`), 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.
20
22
  - **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.
21
23
  - **The key is never yours**: it goes from the command's own process to the host and to Impeccable, and into no message, status line, file, command or report. `vercel project protection`, `vercel api`, `vercel curl` and `netlify api` print it, and the fence refuses them.
@@ -6,13 +6,13 @@ Every run, once the settings are saved. The person merges the setup pull request
6
6
 
7
7
  1. **The setup commit**, when the repository needs a change to keep previews working: the workflow and any fix the build needed, nothing else. With the workflow current on the default branch and no fix, there is none. Check access as workflow-as-person says, branch `impeccable/setup` from `origin/<defaultBranch>`, and bring in the workflow and commit as that case says. Push it in step 7, after the test drive. Done when that case's steps are done.
8
8
  2. **The test drive page**, always. The test drive shows the person a whole review: pages to capture, a finding for each measure, and a new page, which is what starts the review's visitor sessions. Branch `impeccable/test-drive` from the setup commit, so it carries the same workflow and its preview runs from its own head (from the default branch when there is no setup commit). Add one small, self-contained page in the repository's own framework, such as `/impeccable-test-drive`: a file where the routing finds pages, plus its styles, with a heading, a paragraph and a few elements in the app's own words (so the review judges it as one of the app's pages), and the flaws below. Link it from a page **a visitor opens without signing in**, the landing page when it is one, and keep the new page open without signing in too: no guard, on the server or in the browser, sends it to the sign-in. Done when the new page carries every flaw below with its comment, and the page you picked links to it.
9
- 3. **Prove both pages** with impeccable_verify_preview, for a preview Impeccable builds itself (ci-tunnel, static-site): the input that proved the preview, with both pages in `paths` for a server (a redirect to the sign-in fails there) or in `entries` for a static site. A host's preview (vercel-previews, vercel-setup, netlify-previews, netlify-setup) is proven on the host's own build in step 4 instead; its case says how the branch is built here first. Done when it answers `verified: true`, or the preview is a host's.
9
+ 3. **Prove both pages** with impeccable_verify_preview, for a preview Impeccable builds itself (static-site): the input that proved the preview, with both pages in `entries`. A host's preview (vercel-previews, vercel-setup, netlify-previews, netlify-setup) is proven on the host's own build in step 4 instead; its case says how the branch is built here first. Done when it answers `verified: true`, or the preview is a host's.
10
10
  4. **Push the test drive and prove its preview**: commit, `git push -u origin impeccable/test-drive`, then the way its preview comes:
11
11
  - **Vercel** (vercel-previews, vercel-setup) builds every push: impeccable_hosted_previews with `branch: "impeccable/test-drive"` waits up to five minutes for its deployment of the pushed head, under a status line with its timer. Prove the test drive's page on that address with impeccable_prove_preview (past Vercel's login as preview-protection says), and save the method as the case says. Then step 5.
12
12
  - **Netlify** (netlify-previews, netlify-setup) builds a deploy preview only for a pull request: open the test drive now as a **draft**, step 5's `gh pr create` with `--draft`, which Impeccable leaves unreviewed. impeccable_netlify_previews with `pull` the number it printed waits up to five minutes for the deploy preview, under a status line with its timer. Prove the test drive's page on it, save the method as the case says, then `gh pr ready impeccable/test-drive --repo <owner>/<repo>` marks it ready, which starts the review. Then step 5's recording.
13
- - **Impeccable's own preview** (ci-tunnel, static-site): the workflow in the branch builds it on the pull request, and Impeccable's review holds up to eight minutes for that job before it renders, so step 3's proof and the workflow in the branch (from the setup commit) are all this step takes. Then step 5.
13
+ - **Impeccable's own preview** (static-site): the workflow in the branch builds it on the pull request, and Impeccable's review holds up to eight minutes for that job before it renders, so step 3's proof and the workflow in the branch (from the setup commit) are all this step takes. Then step 5.
14
14
 
15
- Done when the test drive's page is proven on its preview, and a host's method saved; when nothing proved it, the reference below says how the run goes on.
15
+ A host's wait that ran out shows its own screen, which opens the host's page where its builds are turned on and waits again on the person's yes. Done when the test drive's page is proven on its preview, and a host's method saved; when nothing proved it, the reference below says how the run goes on.
16
16
  5. **Open the test drive and record it**: `gh pr create --repo <owner>/<repo> --head impeccable/test-drive --base <defaultBranch>` with the title "Impeccable test drive (safe to close)" and a body that starts "This pull request only shows Impeccable reviewing a change. Close it when you are done; nothing here needs to be merged." and then names the two pages and lists the flaws planted for the review to find, one line each (a Netlify draft opened this way in step 4, and was marked ready there). Record it right away with impeccable_test_pr, so the web page follows its review; it refuses a draft. Done when impeccable_test_pr answered "Recorded".
17
17
  6. **Check the test drive's preview once**, while it is the newest open pull request: impeccable_write to `latest-pull-check` with the app's `appKey`. Done when it answered; this early a 409 `no-preview-running` or a job still starting is expected, and the preview goes under "What is not verified".
18
18
  7. **Open the setup pull request**, when there is a setup commit: `git push -u origin impeccable/setup`, then `gh pr create --repo <owner>/<repo> --head impeccable/setup --base <defaultBranch>` with the title "Set up Impeccable previews" and a body that starts "Merge this to keep Impeccable's previews working." and then says what it carries and why, one line each. Record it with impeccable_test_pr: the test drive again, and this one in `setup_pr`. Done when impeccable_test_pr answered "Recorded".
@@ -26,6 +26,6 @@ Every run, once the settings are saved. The person merges the setup pull request
26
26
  - **Low-contrast text**: text in a solid light grey, well under 4.5:1 against its background.
27
27
  - **A layout transition**: `transition: width 0.3s`, naming the layout property.
28
28
  - **Body text at the viewport edge**: a paragraph of more than 40 characters in the main content, without a background, flush with the sides of the viewport.
29
- - **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left`. The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report, and put "Mark the test drive #N ready once Netlify builds its preview" in `left`.
29
+ - **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left`. The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report. Put "Mark the test drive #N ready once Netlify builds its preview" in `left` only where Netlify built no preview; where it built one and the lock stayed in front, preview-protection's step 3 says what the report says instead.
30
30
  - **A single-page app**, whose routes live in the browser: the new page is a self-contained HTML file in the folder the build copies as it is (`public/` in Vite), so the output holds it for `entries` and the review counts it as a page added.
31
31
  - **A branch name an earlier run took** (checkout or push says so): use `-2`, then `-3`, and name the earlier pull request in the report so the person can close it.
@@ -0,0 +1,17 @@
1
+ ### When this applies
2
+
3
+ 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), and no host's previews show them: no host case applied, or one ended without a preview. Impeccable no longer runs a server in CI for a new setup, so such pages are previewed only by their host's pull request previews. These steps replace the playbook's step 3.
4
+
5
+ ### Steps
6
+
7
+ 1. **Ask where the previews come from**, 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 (the sign-in skipped, a step failed): ask nothing, and go on as after a skip.
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**: 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 the pages need a server and no host deploys previews of them, 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 the skipped 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
+
13
+ ### Reference
14
+
15
+ - **Already on Impeccable's tunnel**: a repository whose state shows `ci-tunnel` as the preview's method, with its workflow committed, keeps working as it is. Change nothing about that preview: a host the person picks here replaces it once its preview is saved, and after a skip the tunnel stays, which the report says.
16
+ - **Never save `ci-tunnel`**: impeccable_write refuses it.
17
+ - The sign-in case applies all the same: the accounts serve the host's previews once they come.
@@ -18,4 +18,3 @@ The app has its own sign-in: a form that takes a username or an email and a pass
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
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 seed sets one, name its file in the account question's hint and let the person type it.
20
20
  - `loginPage` is the login form's path when the code names one, such as `/login`.
21
- - A `ci-tunnel` preview runs the seed, so the seed's accounts exist there.
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- The pages need no server from this repository: plain HTML, or a build that writes a complete static site. Pages that need SSR, API routes or a backend here follow ci-tunnel; an external API they call does not change that. A build that writes only JavaScript, CSS or types is a library, not a site.
3
+ The pages need no server from this repository: plain HTML, or a build that writes a complete static site. Pages that need SSR, API routes or a backend here follow server-previews; an external API they call does not change that. A build that writes only JavaScript, CSS or types is a library, not a site.
4
4
 
5
5
  ### Steps
6
6
 
@@ -10,7 +10,7 @@ Vercel deploys previews of the pull requests: impeccable_hosted_previews lists a
10
10
  ### Reference
11
11
 
12
12
  - **Behind Vercel's login** (`gate: "vercel-protection"`): Deployment Protection stands in front of the previews. Get past it as preview-protection says, which proves the preview, and go on with step 2; when nothing got past it, that case says how the run ends.
13
- - **Another app, or no answer from any of the three**: Vercel's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Vercel address answered.
13
+ - **Another app, or no answer from any of the three**: Vercel's previews do not show this app. Set the preview up as server-previews or static-site says instead, and say under "What is not verified" what the Vercel address answered.
14
14
  - **The pull requests**: no setup pull request; the test drive branches from the default branch, and Vercel builds its preview from the push, which pull-requests' step 4 waits for and proves the test drive's page on before the pull request opens. In place of pull-requests' step 3, build the test drive branch once with the install and build Vercel runs (`vercel.json`'s `installCommand` and `buildCommand`, else the package's), as plain commands (`pnpm install`, then `pnpm run build`) and not with impeccable_verify_preview, which is for a preview Impeccable starts itself, so a page that does not build is never pushed.
15
15
  - The evidence is GitHub's: link, deploy and change nothing on Vercel, and save no address pattern, since Vercel reports each preview's address to GitHub itself.
16
16
  - The report names the address that answered with this app under "What we found", and puts the test drive's own preview under "What is not verified".
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `vercel` dependency or script) and Vercel has never deployed it: impeccable_hosted_previews lists no deployment that Vercel made (`vercel: true`), and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in vercel-previews, the repository needs no workflow and no setup pull request.
3
+ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `vercel` dependency or script), or the person picked Vercel in server-previews, and Vercel has never deployed it: impeccable_hosted_previews lists no deployment that Vercel made (`vercel: true`), and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in vercel-previews, the repository needs no workflow and no setup pull request.
4
4
 
5
5
  ### Steps
6
6
 
@@ -8,22 +8,22 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
8
8
  - **`cli: "missing"`**: install the CLI with the person's package manager (`npm install -g vercel`, `pnpm add -g vercel`, `yarn global add vercel` or `bun add -g vercel`), then ask again: the screen shows no second time.
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**: set the preview up as ci-tunnel or static-site says, and say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
11
+ - **Skip**: set the preview up as server-previews or static-site says, and 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** (run each Vercel command plain: its answer is short, and a pipe to `tail` is refused): `vercel link --yes --team <the team>` creates the project in that team, named after the folder (or links the one that has that name), and connects the GitHub `origin`; the CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when it prints the linked project.
13
- 3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, 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. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
13
+ 3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, 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.
14
14
  4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes vercel-previews' reference gives: from the default branch, no setup pull request, built once with plain commands. Stage its files by name: `vercel link` writes `.vercel/` and a `.env.local` that holds a Vercel token, and may add `.vercel` to `.gitignore`, and none of that goes into the pull request. Its push starts the project's first deployment. Done when `git push -u origin impeccable/test-drive` succeeded.
15
15
  5. **Its preview, proven and saved, then its pull request** as pull-requests' step 4 (Vercel) and step 5 say: the wait for the pushed head's deployment, the test drive's page proven on the address it lists (past Vercel's login as preview-protection says), impeccable_write to `preview` as vercel-previews' step 2 says, then the pull request and its 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
 
17
17
  ### Reference
18
18
 
19
- - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the team skipped, the project not created, the repository not connected): the Vercel setup ends there. Set the preview up as ci-tunnel or static-site 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.
19
+ - **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the team skipped, the project not created, the repository not connected): the Vercel setup ends there. Set the preview up as server-previews or static-site 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.
20
20
  - **A push before step 3** is refused, since a connected project builds every push: run impeccable_vercel_connect first.
21
- - **`locked: false`** from step 3, with `production` not "exists": 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, say under "What was skipped, and why" what stopped the lock (the answer's `reason`) and how the person locks the project (Vercel Authentication for All Deployments, under the project's Settings → Deployment Protection), and put "Lock the Vercel project, then run the setup again" in `left`.
21
+ - **`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`).
22
22
  - **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.
23
- - **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed, so the report can name the cause.
24
- - **No deployment within five minutes**: the build had not finished, or Vercel builds no push here: the project's Git connection is the thing to check, and "Run the setup again once Vercel builds the repository" goes in `left`.
23
+ - **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.
24
+ - **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.
25
25
  - **Behind Vercel's login** (`gate: "vercel-protection"`) with nothing getting past it: preview-protection says what the report names.
26
- - **The first deployment** of a new project is called Production, whatever branch it builds. Locked in step 3, and still so when its deployment is read, it is the test drive's preview: impeccable_hosted_previews lists it, and the proof opens it past Vercel's login as preview-protection says. Unlocked by then, or another project's, the evidence lists no address for it and the proof refuses it: no test drive, as "No preview for the pushed test drive" says, and the report says under "What is not verified" that Vercel serves the test drive's build at the project's own address (`vercel project ls` prints it) without a login until the default branch deploys, with the step from `locked: false` in `left`. A project that deployed production before the run (`production: "exists"`) builds the push as a Preview, and keeps its addresses as they were.
26
+ - **The first deployment** of a new project is called Production, whatever branch it builds. Locked in step 3, and still so when its deployment is read, it is the test drive's preview: impeccable_hosted_previews lists it, and the proof opens it past Vercel's login as preview-protection says. Unlocked by then, or another project's, the evidence lists no address for it and the proof refuses it: no test drive, as "No preview for the pushed test drive" says, and the report says under "What is not verified" that Vercel serves the test drive's build at the project's own address (`vercel project ls` prints it) without a login until the default branch deploys, and that Vercel Authentication for All Deployments, under the project's Settings → Deployment Protection, puts its login back in front. A project that deployed production before the run (`production: "exists"`) builds the push as a Preview, and keeps its addresses as they were.
27
27
  - **Behind Vercel's login** (`gate: "vercel-protection"`): a project the CLI creates starts with Vercel Authentication on, and step 3 locks it at every address, so expect it, and go on as the preview-protection case says.
28
28
  - **The report** names the Vercel project and its team under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Vercel account, and the CLI's install when there was one, and that the link wrote a `.env.local` with a Vercel token in the checkout. A project locked in step 3 gets this too: Vercel's login stands in front of every address of it, its production one included, until the person chooses Standard Protection under Settings → Deployment Protection, which opens production and keeps the previews behind the login. When the answer's `productionBranch` is not the repository's default branch, say under "Before you merge" that Vercel deploys production from that branch (Settings → Environments → Production → Branch Tracking changes it). The project's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
29
29
  - **Status lines**: the sign-in, the link, the connection and the lock belong to the Preview method task; from the page on, the Test drive task, so name the test drive in each line about its build, its push, the wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Vercel's login as a login, since a line about signing in belongs to the Sign-in task.
package/dist/playbook.js CHANGED
@@ -12,13 +12,13 @@ export function loadCases(directory = new URL('./playbook/cases/', import.meta.u
12
12
  }
13
13
  /** 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
14
  export function playbook(repo) {
15
- 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) or building it itself through the workflow the setup state generates (\`ci-tunnel\` for pages that need a server, \`static-upload\` for a static site), 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.
15
+ 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) or, for a static site, building it itself through the workflow the setup state generates (\`static-upload\`), 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.
16
16
 
17
17
  # Steps
18
18
 
19
19
  1. **Read the state.** The setup API's instructions and state came with the first message, trimmed to this setup; impeccable_state reads the state again. Done when you know the preview item's status, \`file\` and \`workflowStanding\`, whether there is a \`signin\` item, whether there is an \`apps\` item (the monorepo case comes first then), and the titles in \`notPartOfThisSetup\`.
20
20
  2. **Read the repository**: manifests, the lockfile, scripts, framework config, CI workflows, env examples, the routes, the auth code and the seed. Done when you can name the package manager, the install, build and start commands, whether the pages need a server, how people sign in, and which pages a visitor opens without signing in.
21
- 3. **Set up the preview** as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests, as the netlify-previews case says when impeccable_netlify_previews lists Netlify's (and as preview-protection says when the host's login or password stands in front of either), as the cloudflare-previews case says when impeccable_cloudflare_previews lists Cloudflare Pages', as the vercel-setup case says when the checkout is set up for Vercel and Vercel has never deployed it, as the netlify-setup case says when the checkout is set up for Netlify and it lists none, else as the ci-tunnel case says when the pages need a server, and as the static-site case says when they do not. Done when that case's steps are done.
21
+ 3. **Set up the preview** as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests, as the netlify-previews case says when impeccable_netlify_previews lists Netlify's (and as preview-protection says when the host's login or password stands in front of either), as the cloudflare-previews case says when impeccable_cloudflare_previews lists Cloudflare Pages', as the vercel-setup case says when the checkout is set up for Vercel and Vercel has never deployed it, as the netlify-setup case says when the checkout is set up for Netlify and it lists none, else as the server-previews case says when the pages need a server, and as the static-site case says when they do not. Done when that case's steps are done.
22
22
  4. **Set up the sign-in** as the sign-in case says, when the app has its own. Done when that case's steps are done.
23
23
  5. **Run impeccable_checkup**, do what \`next\` says, and run it again. Done when \`status\` is complete, or all that is left is in \`forPerson\` or \`waitingFor\`.
24
24
  6. **Open the pull requests** as the pull-requests case says. What waits on the person holds neither back. Done when that case's steps are done.
@@ -39,6 +39,7 @@ The state names the setup page's other cards in \`notPartOfThisSetup\`. They are
39
39
  - 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.
40
40
  - Ask the questions that belong together back to back, one call each.
41
41
  - Every question offers Skip: after a skip, carry on without the answer, and say in the report what it left undone.
42
+ - **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.
42
43
 
43
44
  ## Commands
44
45
  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.
@@ -57,7 +58,7 @@ Keep secrets out of files, settings and messages: the workflow names a repositor
57
58
  ## Voice
58
59
  The person reads your [STATUS] lines, task titles and questions, here and on the web page: calm, specific, one line each.
59
60
  - **[STATUS] lines**: start each step's message with one, a present-tense verb phrase of at most 70 characters saying what is happening (*"Running the build"*, *"Opening the test drive"*), or a short finding (*"Next.js 16, pnpm, Prisma on SQLite. No hosting found."*). impeccable_progress refuses a line that narrates like an assistant: write *"Reading the deploy workflows"*, not *"I'm trying to work out how the app deploys."* Detail belongs in the report.
60
- - **Tasks**: after each task, call impeccable_progress with the whole list. Titles are short noun phrases the person recognizes, the same every run where they fit: Read the repository, Install and build, Preview method, Preview workflow, Sign-in, Test drive, Setup pull request. One task is in progress at a time, and every status line is the detail of that task (*"Installing with pnpm"* while Install and build is in progress); impeccable_progress refuses a status line about another task.
61
+ - **Tasks**: after each task, call impeccable_progress with the whole list. Titles are short noun phrases the person recognizes, the same every run where they fit: Read the repository, Install and build, Preview method, Preview workflow, Sign-in, Test drive, Setup pull request. The list carries only what this run does: add a task when its case starts, never before, and leave off a task no case of this run takes, such as Preview workflow and Setup pull request where a host builds the previews (impeccable_progress refuses them once its previews are the method). Mark a task skipped only when the person skipped it, and failed only when it was tried and failed. One task is in progress at a time, and every status line is the detail of that task (*"Installing with pnpm"* while Install and build is in progress); impeccable_progress refuses a status line about another task.
61
62
  - **Questions**: one sentence, at most one hint line, and a title that names the subject.
62
63
 
63
64
  ## The report
package/dist/tools.js CHANGED
@@ -7,7 +7,7 @@ import { request } from 'node:http';
7
7
  import { createServer } from 'node:net';
8
8
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
9
9
  import { defineTool } from '@earendil-works/pi-coding-agent';
10
- import { agreeToHostSetup, bindPreviewPullRequest, chooseHostTeam, classifyPreviewCommand, endHostSetup, hostSetupAgreed, isSecretFile, observePreviewAttempts, parseCommand, previewRetryState, settleVercelFirstDeployment } from './fence.js';
10
+ import { agreeToHostSetup, bindPreviewPullRequest, chooseHostTeam, chosenHostTeam, classifyPreviewCommand, endHostSetup, hostSetupAgreed, isSecretFile, observePreviewAttempts, parseCommand, previewRetryState, settleVercelFirstDeployment } from './fence.js';
11
11
  import { requestJson } from './http.js';
12
12
  import { appsLeftOut, focusCheckup, focusInstructions, focusState, severalApps } from './focus.js';
13
13
  import { HOST_QUESTIONS, HOST_SCREENS } from './host-questions.js';
@@ -16,7 +16,7 @@ import { cfRunner, pagesProject, readCloudflarePreviews } from './cloudflare.js'
16
16
  import { passCloudflareAccess } from './cloudflare-access.js';
17
17
  import { commandRunner, connectNetlifySite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
18
18
  import { basicCredentials, PASSWORD_HEADER, passNetlifyLock } from './netlify-password.js';
19
- import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, vercelStatus, vercelTeams, waitForVercelAccess } from './vercel.js';
19
+ import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, vercelProjectPage, vercelStatus, vercelTeams, waitForVercelAccess, waitForVercelLock } from './vercel.js';
20
20
  import { BYPASS_HEADER, passVercelLogin, readLink } from './vercel-bypass.js';
21
21
  import { fitsTask, stagesOf } from './stages.js';
22
22
  import { redact } from './log.js';
@@ -1419,26 +1419,37 @@ export function createTools(ctx) {
1419
1419
  /** The host whose preview a proof answered with this app, and the one whose previews impeccable_write then saved as the method. */
1420
1420
  let provenHost;
1421
1421
  let savedHost;
1422
+ /** Whether the repository's last saved method is a host's previews, and whether any write named an app: a monorepo's apps each go their own way, and only a run whose host builds them all opens no workflow and no setup pull request. */
1423
+ let hostMethod = false;
1424
+ let perApp = false;
1422
1425
  /**
1423
1426
  * A host's build, waited for on the screen as the task in progress, naming `host`, with the time so far: `read` again
1424
1427
  * every so often until `built` says the evidence is final, or the wait runs out. The status line names the host and what it builds.
1425
1428
  */
1426
- async function awaitBuild(host, status, read, built, signal) {
1427
- let evidence = await read();
1428
- if (built(evidence))
1429
- return evidence;
1429
+ async function awaitBuild(host, status, read, built, page, signal) {
1430
+ const first = await read();
1431
+ if (built(first))
1432
+ return first;
1430
1433
  const wait = ctx.previewWait ?? { totalMs: 300_000, everyMs: 15_000 };
1431
- said(status);
1432
- reportWait({ since: Date.now(), host });
1433
- try {
1434
- for (const end = Date.now() + wait.totalMs; !built(evidence) && Date.now() < end; evidence = await read()) {
1435
- await sleep(wait.everyMs, undefined, { signal });
1434
+ const waited = async (evidence) => {
1435
+ said(status);
1436
+ reportWait({ since: Date.now(), host });
1437
+ try {
1438
+ for (const end = Date.now() + wait.totalMs; !built(evidence) && Date.now() < end; evidence = await read()) {
1439
+ await sleep(wait.everyMs, undefined, { signal });
1440
+ }
1436
1441
  }
1437
- }
1438
- finally {
1439
- reportWait(null);
1440
- }
1441
- return evidence;
1442
+ finally {
1443
+ reportWait(null);
1444
+ }
1445
+ return evidence;
1446
+ };
1447
+ // No build in the time: the host's page where a build is turned on, and the wait again on the person's yes.
1448
+ return onHostPage(`${host.toLowerCase()}-build`, page, (e) => !built(e), await waited(first), async () => waited(await read()), {
1449
+ skipped: `The person stopped waiting for ${host}'s build.`,
1450
+ still: `${host} built no preview in the time.`,
1451
+ stopped: `The run stopped while ${host}'s build was waited for.`,
1452
+ }, signal);
1442
1453
  }
1443
1454
  /** What a wait for a pull request's build is for: the test drive's preview, or another pull request's. */
1444
1455
  const subjectOf = async (pull) => {
@@ -1458,7 +1469,7 @@ export function createTools(ctx) {
1458
1469
  /** The Netlify site password saved in this run, for the proof of that site's addresses to send; never for the model. It is one site's, so it goes only to that site's deploy previews. */
1459
1470
  let sitePassword;
1460
1471
  const passwordSites = new Set();
1461
- /** What impeccable_netlify_password answered for a site, less the password, where the answer stands for the run: a skip is asked once, and the team login stays. Another site of the repository is read on its own. */
1472
+ /** What impeccable_netlify_password answered for a site, less the password, where the answer stands for the run: a skip is asked once, the team login kept after the retries stays, and a site made public stays public. Another site of the repository is read on its own. */
1462
1473
  const passwordStands = new Map();
1463
1474
  /** The sites whose password the person said yes to storing: a later call for the same site, after a page the app's own sign-in answered, asks no second time. */
1464
1475
  const passwordConsented = new Set();
@@ -1542,6 +1553,9 @@ export function createTools(ctx) {
1542
1553
  endHostSetup(ctx.log, host);
1543
1554
  };
1544
1555
  const HOST_NAMES = { netlify: 'Netlify', vercel: 'Vercel' };
1556
+ /** The Vercel project and the Netlify site this run set up, by name, for the pages of theirs a screen opens. */
1557
+ let vercelProject;
1558
+ let netlifySite;
1545
1559
  const hostCli = (host, pi, signal) => {
1546
1560
  const runner = (browser = false) => (ctx.hostRunner ?? commandRunner)(host, pi.cwd, { signal, browser, timeoutMs: browser ? 300_000 : 30_000 });
1547
1561
  const cli = runner();
@@ -1622,25 +1636,37 @@ export function createTools(ctx) {
1622
1636
  }
1623
1637
  }
1624
1638
  /**
1625
- * The one-time access a host's GitHub app needs when it cannot reach the repository: a screen that opens the app's page
1626
- * on GitHub, a wait of up to five minutes for the host to reach it (`reaches`, tried again every so often), a retry
1627
- * screen, and after two failed retries the connection left to the person.
1639
+ * What the person does on a host's page, asked in the run: the screen `screen` (then `<screen>-retry`) opens the page,
1640
+ * `again` waits up to five minutes for it to be done, trying every so often, and after two failed retries the step is
1641
+ * left undone with `words.still`. `pending` says it is not done yet; the run carries on either way.
1628
1642
  */
1629
- async function hostAccess(host, result, reaches, signal) {
1630
- const name = HOST_NAMES[host];
1631
- for (let retries = 0; result.access && !signal?.aborted; retries++) {
1632
- if ((await ask(HOST_SCREENS[retries ? `${host}-access-retry` : `${host}-access`])).skipped)
1633
- return { ...result, reason: `The person skipped giving ${name}'s GitHub app access to the repository.` };
1634
- (ctx.openBrowser ?? openBrowser)(result.access);
1635
- said(`Waiting for ${name} to reach the repository`);
1636
- result = await reaches();
1643
+ async function onHostPage(screen, page, pending, result, again, words, signal) {
1644
+ for (let retries = 0; pending(result) && !signal?.aborted; retries++) {
1645
+ if ((await ask(HOST_SCREENS[retries ? `${screen}-retry` : screen])).skipped)
1646
+ return { ...result, reason: words.skipped };
1647
+ const at = page(result);
1648
+ if (at)
1649
+ (ctx.openBrowser ?? openBrowser)(at);
1650
+ if (words.waiting)
1651
+ said(words.waiting);
1652
+ result = await again(result);
1637
1653
  if (signal?.aborted)
1638
- return { ...result, reason: `The run stopped while ${name} was waited for.` };
1639
- if (result.access && retries === 2)
1640
- return { ...result, reason: `${name}'s GitHub app still cannot reach the repository.` };
1654
+ return { ...result, reason: words.stopped };
1655
+ if (pending(result) && retries === 2)
1656
+ return { ...result, reason: words.still };
1641
1657
  }
1642
1658
  return result;
1643
1659
  }
1660
+ /** The one-time access a host's GitHub app needs when it cannot reach the repository, given on the app's page on GitHub while `reaches` tries again. */
1661
+ function hostAccess(host, result, reaches, signal) {
1662
+ const name = HOST_NAMES[host];
1663
+ return onHostPage(`${host}-access`, (r) => r.access, (r) => Boolean(r.access), result, reaches, {
1664
+ waiting: `Waiting for ${name} to reach the repository`,
1665
+ skipped: `The person skipped giving ${name}'s GitHub app access to the repository.`,
1666
+ still: `${name}'s GitHub app still cannot reach the repository.`,
1667
+ stopped: `The run stopped while ${name} was waited for.`,
1668
+ }, signal);
1669
+ }
1644
1670
  /** The connection of the linked Netlify site with this repository, through Netlify's site settings and its GitHub app, with the app's access asked when it cannot read the repository. */
1645
1671
  async function netlifyConnect(pi, settings, signal) {
1646
1672
  // The site's settings change on the host: only once the person opened the Netlify setup, as the fence holds for the CLI.
@@ -1650,6 +1676,7 @@ export function createTools(ctx) {
1650
1676
  const gh = (ctx.hostRunner ?? commandRunner)('gh', pi.cwd, { signal });
1651
1677
  const clone = ctx.hostWait?.clone;
1652
1678
  const result = await connectNetlifySite(ctx.owner, ctx.repo, pi.cwd, { netlify, gh, settings, signal, ...(clone ? { wait: clone } : {}) });
1679
+ netlifySite = result.site?.name ?? netlifySite;
1653
1680
  // A build every 30 s until one reads the repository: the person is giving the app access meanwhile.
1654
1681
  return hostAccess('netlify', result, async () => (await waitForNetlifyAccess(result.site.id, { netlify, signal, wait: ctx.hostWait?.access ?? { totalMs: 300_000, everyMs: 30_000 }, ...(clone ? { clone } : {}) })
1655
1682
  ? { connected: true, site: result.site, how: 'github-app' } : result), signal);
@@ -1673,7 +1700,18 @@ export function createTools(ctx) {
1673
1700
  settleVercelFirstDeployment(ctx.log, { projectId: link?.projectId ?? null, how: 'connection' });
1674
1701
  return result;
1675
1702
  }
1676
- const first = link ? await prepareFirstDeployment(vercel, link) : { production: 'unknown', locked: false, reason: 'This folder is linked to no Vercel project (.vercel/project.json): run vercel link first.' };
1703
+ let first = link ? await prepareFirstDeployment(vercel, link) : { production: 'unknown', locked: false, reason: 'This folder is linked to no Vercel project (.vercel/project.json): run vercel link first.' };
1704
+ vercelProject = first.project ?? vercelProject;
1705
+ // Vercel refused the lock: the person turns it on in the project's settings, in this run, and the read sees it.
1706
+ if (link && first.production === 'none' && !first.locked) {
1707
+ const page = vercelProjectPage('settings/deployment-protection', chosenHostTeam(ctx.log, 'vercel'), vercelProject);
1708
+ first = await onHostPage('vercel-lock', () => page, (f) => f.production !== 'exists' && !f.locked, first, () => waitForVercelLock(vercel, link, { wait: ctx.hostWait?.access ?? { totalMs: 300_000, everyMs: 10_000 }, signal }), {
1709
+ waiting: "Waiting for Vercel's login on the project",
1710
+ skipped: 'The person skipped locking the Vercel project.',
1711
+ still: "Vercel's login is still not in front of the project's addresses.",
1712
+ stopped: 'The run stopped while the lock was waited for.',
1713
+ }, signal);
1714
+ }
1677
1715
  if (link && (first.production === 'exists' || first.locked === true))
1678
1716
  settleVercelFirstDeployment(ctx.log, { projectId: link.projectId, how: 'project' });
1679
1717
  return { ...result, ...first };
@@ -1755,7 +1793,7 @@ export function createTools(ctx) {
1755
1793
  return [
1756
1794
  tool('impeccable_workflow_access', 'Check workflow access', 'Check that the setup pull request can carry the workflow: reads the active GitHub CLI account and its scopes, the origin remote and the default branch, and refreshes origin/<defaultBranch>. Call it before branching. needsWorkflowScope true means the account lacks workflow access over HTTPS; base.status "refreshed" means origin/<defaultBranch> is current. Only the push itself proves Git may push.', NO_INPUT, (_p, pi, signal) => workflowAccessTool(pi, signal)),
1757
1795
  tool('impeccable_sync_workflow', 'Update the preview workflow', 'Write the state\'s workflow to .github/workflows/impeccable-preview.yml byte for byte and remove the retired workflow the state names, after checking access as impeccable_workflow_access does. Call it once the settings are saved, and again after committing: stagedMatches and committedMatches true (and retiredRemoval staged and committed) prove the commit holds the exact bytes; null means nothing was compared. It refuses, changing nothing, when the file has the person\'s local edits. It never branches, stages, commits or pushes.', NO_INPUT, (_p, pi, signal) => syncWorkflowTool(pi, signal)),
1758
- tool('impeccable_verify_preview', 'Check the local preview', 'Prove the preview on this machine before saving it: runs the steps in a throwaway copy of the checkout, with a throwaway database, and removes everything afterwards. For a server give start, port, paths and host: it starts the app and requests each path as localhost and as the tunnel host. For a static site give output: it checks the build wrote the entry pages. verified covers HTTP answers and files only, never rendering, sign-in or the CI runtime; a failure names its stage.', PREVIEW_VERIFICATION, async (p, pi, signal) => JSON.stringify(await verifyLocalPreview(p, pi.cwd, signal, ctx.sandbox))),
1796
+ tool('impeccable_verify_preview', 'Check the local preview', 'Prove the preview on this machine before saving it: runs the steps in a throwaway copy of the checkout, with a throwaway database, and removes everything afterwards. For a server already on Impeccable\'s tunnel give start, port, paths and host: it starts the app and requests each path as localhost and as the tunnel host. For a static site give output: it checks the build wrote the entry pages. verified covers HTTP answers and files only, never rendering, sign-in or the CI runtime; a failure names its stage.', PREVIEW_VERIFICATION, async (p, pi, signal) => JSON.stringify(await verifyLocalPreview(p, pi.cwd, signal, ctx.sandbox))),
1759
1797
  tool('impeccable_preview_run', 'Read the preview attempt', 'On a rerun, once impeccable_test_pr recorded the setup pull request: read its preview workflow\'s runs. Answers each attempt with its failed stage and a fixed reason (never log text), the three-attempt budget (used, remaining, historyKnown), and adds the attempts to the task list. Reading is free.', PREVIEW_RUN, async (p, pi, signal) => {
1760
1798
  if (!retry.pr)
1761
1799
  return 'Refused: record the setup pull request with impeccable_test_pr first.';
@@ -1832,6 +1870,9 @@ export function createTools(ctx) {
1832
1870
  return `Refused: ${target.error}`;
1833
1871
  if (/\/(preview-auth\/[^/]+|preview-protection)$/.test(target.url))
1834
1872
  return "Refused: a host's key is saved by impeccable_vercel_bypass (a Vercel bypass secret), impeccable_netlify_password (a Netlify site password) or impeccable_cloudflare_access (a Cloudflare Access service token), which opens the preview with it first, and never through this tool. Nothing was saved.";
1873
+ if (target.url.endsWith('/preview') && object(p.body)?.intendedMethod === 'ci-tunnel') {
1874
+ return "Refused: Impeccable no longer runs a server in CI for a new setup, so ci-tunnel is never saved. Pages that need a server get their host's pull request previews, as the server-previews case says. Nothing was saved.";
1875
+ }
1835
1876
  if (target.url.endsWith('/preview') && object(p.body)?.intendedMethod === 'preview-deploy' && !hostedPreviewProven) {
1836
1877
  return "Refused: prove a preview address with impeccable_prove_preview first: the host's previews become the method only once one answered with this app. Nothing was saved.";
1837
1878
  }
@@ -1855,10 +1896,14 @@ export function createTools(ctx) {
1855
1896
  const response = await call(pi, target.url, { method: target.method, body });
1856
1897
  if (target.url.endsWith('/preview') && response.ok) {
1857
1898
  const keys = object(object(response.data)?.keys);
1858
- if (keys?.intendedMethod === 'stored' && ['ci-tunnel', 'static-upload'].includes(String(object(p.body)?.intendedMethod)))
1899
+ if (keys?.intendedMethod === 'stored' && object(p.body)?.intendedMethod === 'static-upload')
1859
1900
  retry.requireHistory = true;
1860
1901
  if (object(p.body)?.intendedMethod === 'preview-deploy')
1861
1902
  savedHost = provenHost;
1903
+ if (saved?.appKey !== undefined)
1904
+ perApp = true;
1905
+ if (saved && 'intendedMethod' in saved)
1906
+ hostMethod = saved.intendedMethod === 'preview-deploy';
1862
1907
  for (const key of Object.keys(object(p.body) ?? {}))
1863
1908
  if (PREVIEW_FIELDS.has(key) && keys?.[key] === 'stored')
1864
1909
  previews.changes.add(key);
@@ -1918,12 +1963,16 @@ export function createTools(ctx) {
1918
1963
  typed.set(reference, (q.kind === 'account' ? answer.password : answer.value) ?? '');
1919
1964
  return JSON.stringify(q.kind === 'account' ? { username: answer.username ?? '', password: reference } : { value: reference });
1920
1965
  }),
1921
- tool('impeccable_progress', 'Progress', 'Show the person where things stand, here and on the web page: the [STATUS] line and the whole task list. It refuses, saving nothing, a status line out of the playbook\'s voice, more than one task in progress, and a status line about another task than the one in progress.', PROGRESS, async (p) => {
1966
+ tool('impeccable_progress', 'Progress', 'Show the person where things stand, here and on the web page: the [STATUS] line and the whole task list. It refuses, saving nothing, a status line out of the playbook\'s voice, more than one task in progress, a status line about another task than the one in progress, and a Preview workflow or Setup pull request task once a host\'s previews are the method.', PROGRESS, async (p) => {
1922
1967
  const status = p.status.trim();
1923
1968
  const tasks = withAttempts(p.tasks);
1924
- const voice = statusVoiceError(status) ?? stageError(status, p.tasks.filter((t) => !t.id.startsWith('preview-attempt-')));
1969
+ const own = p.tasks.filter((t) => !t.id.startsWith('preview-attempt-'));
1970
+ const voice = statusVoiceError(status) ?? stageError(status, own);
1925
1971
  if (voice)
1926
1972
  return voice;
1973
+ const unneeded = hostMethod && !perApp && !ctx.sync.payload.setup_pr ? own.filter((t) => /\bsetup pull request\b/i.test(t.title) || stagesOf(t.title).includes('Preview workflow')) : [];
1974
+ if (unneeded.length)
1975
+ return `Refused: ${savedHost ?? 'the host'} builds this run's previews, so the repository needs no workflow and no setup pull request. Leave ${unneeded.map((t) => `"${t.title}"`).join(' and ')} off the list: a task the run never does is not shown, not even as skipped. Nothing was saved: call impeccable_progress again.`;
1927
1976
  ctx.sync.update({ phase: 'running', status, tasks });
1928
1977
  reportStatus(status);
1929
1978
  reportTasks(tasks);
@@ -1976,16 +2025,17 @@ export function createTools(ctx) {
1976
2025
  if (!left.includes('Watch the review on the test drive'))
1977
2026
  left.push('Watch the review on the test drive');
1978
2027
  }
1979
- const { phase, status, question, report_markdown, test_pr } = ctx.sync.payload;
2028
+ const { phase, status, question, report_markdown, test_pr, left: leftBefore } = ctx.sync.payload;
1980
2029
  // A test drive branched before the setup branch was corrected ran on the old head: it shows nothing about this one.
1981
2030
  const covered = test_pr && recordedBranches?.setup
1982
2031
  ? await (ctx.driveCoversSetup ?? driveCoversSetup)(pi.cwd, recordedBranches.setup, recordedBranches.drive) : null;
1983
2032
  const drive = covered === false ? undefined : test_pr;
1984
- ctx.sync.update({ phase: 'completed', status: 'Setup finished', question: null, report_markdown: markdown, test_pr: drive });
2033
+ // The web page lists what is left too, within the Worker's 20 lines (REPORT and the check above keep each line non-empty and short).
2034
+ ctx.sync.update({ phase: 'completed', status: 'Setup finished', question: null, report_markdown: markdown, test_pr: drive, left: left.slice(0, 20) });
1985
2035
  const res = await ctx.sync.flush();
1986
2036
  if (res && res.status >= 400 && res.status < 500) {
1987
2037
  // A definite no: the run is still open on the server, so the session goes back to what it was and the agent can send the report again.
1988
- ctx.sync.update({ phase, status, question, report_markdown, test_pr });
2038
+ ctx.sync.update({ phase, status, question, report_markdown, test_pr, left: leftBefore });
1989
2039
  return `Error: the report was refused (${res.status}: ${res.error}). Fix what it says and call impeccable_report again.`;
1990
2040
  }
1991
2041
  // Otherwise final: a completed write whose answer was lost may still have landed, so it is never taken back.
@@ -1994,7 +2044,7 @@ export function createTools(ctx) {
1994
2044
  const saved = res?.ok ? 'Saved.' : `The web page did not take it (${res?.error ?? 'no answer'}).`;
1995
2045
  return { text: `${saved} The terminal shows the summary and the command exits.`, terminate: true };
1996
2046
  }),
1997
- tool('impeccable_hosted_previews', 'Hosted previews', 'Read the deployments GitHub has on record for this repository: the 20 newest, each with its latest status, its address, its environment, whether Vercel made it and the pull requests whose commit it built. An error says nothing either way. With branch (a branch of this run, pushed, before its pull request exists), only its head\'s, waiting up to five minutes for Vercel\'s deployment of it to finish building, under a status line with its timer. A Production deployment of that branch (a project\'s first is, whatever branch it builds) is listed as its preview only while the project this folder is linked to is locked at every address (impeccable_vercel_connect locks it) and made it, read from Vercel at that moment, since a production deployment is otherwise served at the project\'s own address without a login; then no proof opens it. It changes nothing on the host and proves nothing about the page.', HOSTED_PREVIEWS, async (p, pi, signal) => {
2047
+ tool('impeccable_hosted_previews', 'Hosted previews', 'Read the deployments GitHub has on record for this repository: the 20 newest, each with its latest status, its address, its environment, whether Vercel made it and the pull requests whose commit it built. An error says nothing either way. With branch (a branch of this run, pushed, before its pull request exists), only its head\'s, waiting up to five minutes for Vercel\'s deployment of it to finish building, under a status line with its timer, and again on the person\'s yes when the time ran out. A Production deployment of that branch (a project\'s first is, whatever branch it builds) is listed as its preview only while the project this folder is linked to is locked at every address (impeccable_vercel_connect locks it) and made it, read from Vercel at that moment, since a production deployment is otherwise served at the project\'s own address without a login; then no proof opens it. It changes nothing on the host and proves nothing about the page.', HOSTED_PREVIEWS, async (p, pi, signal) => {
1998
2048
  const branch = p.branch;
1999
2049
  const read = () => (ctx.hostedPreviewReader ?? readHostedPreviews)(ctx.owner, ctx.repo, { signal, branch });
2000
2050
  // With `branch` the evidence holds only the deployments of its head, and Vercel's is its build whatever environment it is called: the first deployment of a
@@ -2003,7 +2053,7 @@ export function createTools(ctx) {
2003
2053
  const ofBranch = (d) => (branch !== undefined ? d.vercel : isPreview(d));
2004
2054
  // A head's build is still building until it has a final status.
2005
2055
  const final = (evidence) => Boolean(evidence.error) || evidence.deployments.some((d) => ofBranch(d) && (d.state === 'success' || d.state === 'failure' || d.state === 'error'));
2006
- const evidence = branch !== undefined ? await awaitBuild('Vercel', `Waiting for Vercel to build ${testDriveBranch(branch) ? "the test drive's preview" : `the preview of ${branch}`}`, read, final, signal) : await read();
2056
+ const evidence = branch !== undefined ? await awaitBuild('Vercel', `Waiting for Vercel to build ${testDriveBranch(branch) ? "the test drive's preview" : `the preview of ${branch}`}`, read, final, () => vercelProjectPage('settings/git', chosenHostTeam(ctx.log, 'vercel'), vercelProject), signal) : await read();
2007
2057
  listedPreviews.clear();
2008
2058
  vercelMade.clear();
2009
2059
  publicProduction.clear();
@@ -2030,20 +2080,25 @@ export function createTools(ctx) {
2030
2080
  }
2031
2081
  return JSON.stringify(evidence);
2032
2082
  }),
2033
- tool('impeccable_netlify_previews', 'Netlify previews', "Read whether this repository's pull requests get Netlify deploy previews: the 10 newest, each with its site, its address, its state and where that was read: GitHub's commit status or Netlify's bot comment on the pull request, or the Netlify CLI on this machine when it is signed in (read only: it never signs in). cli says whether that CLI is missing, signed out, signed in or did not answer. An error says something could not be read, which is unknown rather than absent. With pull (a pull request of this run, a draft included: Netlify builds a deploy preview only for a pull request), only that pull request's, waiting up to five minutes for one to finish building, under a status line with its timer. It changes nothing on Netlify and proves nothing about the page.", NETLIFY_PREVIEWS, async (p, pi, signal) => {
2083
+ tool('impeccable_netlify_previews', 'Netlify previews', "Read whether this repository's pull requests get Netlify deploy previews: the 10 newest, each with its site, its address, its state and where that was read: GitHub's commit status or Netlify's bot comment on the pull request, or the Netlify CLI on this machine when it is signed in (read only: it never signs in). cli says whether that CLI is missing, signed out, signed in or did not answer. An error says something could not be read, which is unknown rather than absent. With pull (a pull request of this run, a draft included: Netlify builds a deploy preview only for a pull request), only that pull request's, waiting up to five minutes for one to finish building, under a status line with its timer, and again on the person's yes when the time ran out. It changes nothing on Netlify and proves nothing about the page.", NETLIFY_PREVIEWS, async (p, pi, signal) => {
2034
2084
  const pull = p.pull;
2035
2085
  const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull });
2036
2086
  // A pull request's preview is still building until Netlify's deploy of it is built or has failed.
2037
2087
  const states = ['success', 'ready', 'failure', 'error'];
2038
2088
  const final = (evidence) => Boolean(evidence.error) || evidence.previews.some((preview) => states.includes(preview.state ?? ''));
2039
- const evidence = pull !== undefined ? await awaitBuild('Netlify', `Waiting for Netlify to build ${await subjectOf(pull)}`, read, final, signal) : await read();
2089
+ const evidence = pull !== undefined ? await awaitBuild('Netlify', `Waiting for Netlify to build ${await subjectOf(pull)}`, read, final, (e) => {
2090
+ // The waited pull request's own site; else the one site known, so another app's site never opens.
2091
+ const known = new Set([...e.sites, ...(netlifySite ? [netlifySite] : [])]);
2092
+ const site = e.previews.find((preview) => preview.pullRequest === pull)?.site ?? (known.size === 1 ? [...known][0] : undefined);
2093
+ return site ? `https://app.netlify.com/projects/${encodeURIComponent(site)}/configuration/deploys#deploy-previews` : 'https://app.netlify.com/';
2094
+ }, signal) : await read();
2040
2095
  netlifyListed.clear();
2041
2096
  for (const p of evidence.previews)
2042
2097
  if (p.state === 'success' || p.state === 'ready')
2043
2098
  netlifyListed.add(new URL(p.url).origin);
2044
2099
  return JSON.stringify(evidence);
2045
2100
  }),
2046
- tool('impeccable_cloudflare_previews', 'Cloudflare Pages previews', "Read whether this repository's pull requests get Cloudflare Pages previews: the 10 newest, each with its address, the branch's alias (https://<branch>.<project>.pages.dev, which follows the branch's latest build), its state and where that was read: the check run or the pull request comment Cloudflare's GitHub app posts, or cf, Cloudflare's CLI, on this machine when it is signed in (read only: it never signs in). cli says whether cf is missing, signed out, signed in or did not answer. An error says something could not be read, which is unknown rather than absent, and unread says whose: GitHub or cf. With pull (a pull request of this run) or branch (a branch of this run, pushed, before its pull request exists), only that one's, waiting up to five minutes for its build to finish, under a status line with its timer. It changes nothing on Cloudflare and proves nothing about the page.", CLOUDFLARE_PREVIEWS, async (p, pi, signal) => {
2101
+ tool('impeccable_cloudflare_previews', 'Cloudflare Pages previews', "Read whether this repository's pull requests get Cloudflare Pages previews: the 10 newest, each with its address, the branch's alias (https://<branch>.<project>.pages.dev, which follows the branch's latest build), its state and where that was read: the check run or the pull request comment Cloudflare's GitHub app posts, or cf, Cloudflare's CLI, on this machine when it is signed in (read only: it never signs in). cli says whether cf is missing, signed out, signed in or did not answer. An error says something could not be read, which is unknown rather than absent, and unread says whose: GitHub or cf. With pull (a pull request of this run) or branch (a branch of this run, pushed, before its pull request exists), only that one's, waiting up to five minutes for its build to finish, under a status line with its timer, and again on the person's yes when the time ran out. It changes nothing on Cloudflare and proves nothing about the page.", CLOUDFLARE_PREVIEWS, async (p, pi, signal) => {
2047
2102
  const pull = p.pull;
2048
2103
  const branch = p.branch;
2049
2104
  if (pull !== undefined && branch !== undefined)
@@ -2053,7 +2108,7 @@ export function createTools(ctx) {
2053
2108
  // GitHub's record unread ends the wait; cf's alone does not, since the check run still says when the build ends.
2054
2109
  const final = (evidence) => Boolean(evidence.unread?.includes('GitHub')) || evidence.previews.some((preview) => preview.state !== 'building');
2055
2110
  const subject = branch !== undefined ? (testDriveBranch(branch) ? "the test drive's preview" : `the preview of ${branch}`) : pull !== undefined ? await subjectOf(pull) : '';
2056
- const evidence = subject ? await awaitBuild('Cloudflare', `Waiting for Cloudflare Pages to build ${subject}`, read, final, signal) : await read();
2111
+ const evidence = subject ? await awaitBuild('Cloudflare', `Waiting for Cloudflare Pages to build ${subject}`, read, final, () => 'https://dash.cloudflare.com/?to=/:account/workers-and-pages', signal) : await read();
2057
2112
  cloudflareListed.clear();
2058
2113
  for (const preview of evidence.previews) {
2059
2114
  if (preview.state !== 'success')
@@ -2100,7 +2155,7 @@ export function createTools(ctx) {
2100
2155
  return 'Refused: publish is a folder relative to the checkout, such as "dist".';
2101
2156
  return JSON.stringify(await netlifyConnect(pi, { command, publish }, signal));
2102
2157
  }),
2103
- tool('impeccable_vercel_connect', 'Connect the repository to Vercel', "Connect the Vercel project this folder is linked to with this repository (vercel git connect), so that pull requests get preview deployments, and lock a project that has no production deployment yet: Vercel makes a new project's first deployment Production, whatever branch it builds, and serves it at the project's own address, so this puts Vercel's login in front of every address of the project (Deployment Protection on All Deployments) before anything is pushed; the fence holds every push until this settled the first deployment. The run does it; when Vercel's GitHub app cannot reach the repository, this asks the person once for its access on GitHub and waits. connected true means the repository is connected, or was already; production \"exists\" means the project deploys production already, so a push of another branch is a Preview; locked true means every address of the project is behind Vercel's login, so its first deployment is no public build; locked false, with reason, means push nothing. A connection left undone answers why, and the GitHub page for the person in access.", NO_INPUT, async (_p, pi, signal) => JSON.stringify(await vercelConnect(pi, signal))),
2158
+ tool('impeccable_vercel_connect', 'Connect the repository to Vercel', "Connect the Vercel project this folder is linked to with this repository (vercel git connect), so that pull requests get preview deployments, and lock a project that has no production deployment yet: Vercel makes a new project's first deployment Production, whatever branch it builds, and serves it at the project's own address, so this puts Vercel's login in front of every address of the project (Deployment Protection on All Deployments) before anything is pushed; the fence holds every push until this settled the first deployment. The run does it; when Vercel's GitHub app cannot reach the repository, this asks the person once for its access on GitHub and waits, and when Vercel refuses the lock, it asks the person once to turn it on in the project's settings and waits. connected true means the repository is connected, or was already; production \"exists\" means the project deploys production already, so a push of another branch is a Preview; locked true means every address of the project is behind Vercel's login, so its first deployment is no public build; locked false, with reason, means push nothing. A connection left undone answers why, and the GitHub page for the person in access.", NO_INPUT, async (_p, pi, signal) => JSON.stringify(await vercelConnect(pi, signal))),
2104
2159
  tool('impeccable_vercel_bypass', "Vercel's login", 'Get past the Vercel login that impeccable_prove_preview met (gate "vercel-protection") in front of a preview Vercel made, the way Vercel documents for tools: a bypass secret of the Vercel project, sent to it in a header. It does the rest with the Vercel CLI signed in as the person: it finds the project\'s secret labelled "Impeccable reviews", or shows one screen asking to create one, or, when the CLI cannot, one screen taking a pasted one; opens the preview with it; and saves it for reviews. Answers {"status": …}: saved (the preview is proven, so the method may be saved; secret says whether it was created, existing or typed, and project which Vercel project holds it), skipped (the person skipped the screen; reason says what came before), refused (no secret opened the preview; reason says what the address answered) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No secret ever comes back.', NO_INPUT, async (_p, pi, signal) => {
2105
2160
  if (gated?.gate !== 'vercel-protection' || !vercelMade.has(new URL(gated.url).origin))
2106
2161
  return 'Refused: this is for a preview that Vercel made, listed by impeccable_hosted_previews, which impeccable_prove_preview answered with gate "vercel-protection". Prove one first.';
@@ -2120,6 +2175,8 @@ export function createTools(ctx) {
2120
2175
  link: () => readLink(pi.cwd),
2121
2176
  consent: async () => !(await ask(HOST_SCREENS['vercel-bypass'])).skipped,
2122
2177
  paste: async () => {
2178
+ // The locked preview may be another project's than the one this run connected, so the person picks the project.
2179
+ (ctx.openBrowser ?? openBrowser)(vercelProjectPage('settings/deployment-protection'));
2123
2180
  const answer = await ask(HOST_SCREENS['vercel-bypass-key']);
2124
2181
  return answer.skipped ? undefined : answer.value?.trim();
2125
2182
  },
@@ -2149,13 +2206,10 @@ export function createTools(ctx) {
2149
2206
  }
2150
2207
  return JSON.stringify(bypassOutcome);
2151
2208
  }),
2152
- tool('impeccable_netlify_password', "Netlify's password", 'Get past the lock that impeccable_prove_preview met (gate "netlify-protection") in front of a Netlify deploy preview, the one way a request can carry Netlify\'s site password: as basic credentials in the Authorization header. It does the rest with the Netlify CLI signed in as the person: it reads the site\'s protection from Netlify; for a password Netlify gives back, it shows one screen asking to store it for Impeccable, or, when the CLI cannot read the site or Netlify does not give the password back, one screen taking a pasted one; opens the preview with it; and saves it for reviews. Answers {"status": …}: saved (the preview is proven, so the method may be saved; password says whether it was existing or typed, and site which Netlify site it is), login (Netlify\'s team login stands in front, which no tool gets past: protection says so, and reason why), skipped (the person skipped the screen; reason says what came before), refused (nothing opened the preview; reason says why) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No password ever comes back.', NO_INPUT, async (_p, pi, signal) => {
2209
+ tool('impeccable_netlify_password', "Netlify's password", 'Get past the lock that impeccable_prove_preview met (gate "netlify-protection") in front of a Netlify deploy preview, the one way a request can carry Netlify\'s site password: as basic credentials in the Authorization header. It does the rest with the Netlify CLI signed in as the person: it reads the site\'s protection from Netlify; for a password Netlify gives back, it shows one screen asking to store it for Impeccable, or, when the CLI cannot read the site or Netlify does not give the password back, one screen taking a pasted one; opens the preview with it; and saves it for reviews. For a site behind Netlify\'s team login it shows one screen asking to make the site public, makes it public as the person, and where Netlify refuses opens Netlify\'s Visitor access page with a screen to try again once the person changed it there. Answers {"status": …}: saved (the preview is proven, so the method may be saved; password says whether it was existing or typed, and site which Netlify site it is), opened (the site is public and the preview proven, so the method may be saved; madePublic says whether the run or the person made it public, and on any other answer that the run made it public all the same), login (Netlify kept its team login in front after the retries; reason says so), skipped (the person skipped the screen; reason says what came before), refused (nothing opened the preview; reason says why) or save-failed (the setup API did not take it all; reason says how far it got: call it once more). No password ever comes back.', NO_INPUT, async (_p, pi, signal) => {
2153
2210
  const site = gated?.gate === 'netlify-protection' ? netlifySiteOf(gated.url) : null;
2154
2211
  if (!gated || !site)
2155
2212
  return 'Refused: this is for a Netlify deploy preview, listed by impeccable_netlify_previews, which impeccable_prove_preview answered with gate "netlify-protection". Prove one first.';
2156
- const stands = passwordStands.get(site);
2157
- if (stands)
2158
- return JSON.stringify(stands);
2159
2213
  const at = gated;
2160
2214
  /** The address pattern the proof with the password answers, saved as the method once the password is. */
2161
2215
  let pattern;
@@ -2165,6 +2219,17 @@ export function createTools(ctx) {
2165
2219
  pattern = proof.pattern;
2166
2220
  return proof;
2167
2221
  };
2222
+ const stands = passwordStands.get(site);
2223
+ if (stands?.status === 'opened') {
2224
+ // A site made public stands while the address opens with nothing sent; behind the login again, it is read again.
2225
+ if ((await prove(undefined)).proven) {
2226
+ provenPattern = pattern ?? provenPattern;
2227
+ return JSON.stringify(stands);
2228
+ }
2229
+ passwordStands.delete(site);
2230
+ }
2231
+ else if (stands)
2232
+ return JSON.stringify(stands);
2168
2233
  // A password in hand already: another deploy preview of the same site opens with it. Another site's lock is read for that site.
2169
2234
  if (passwordSaved && sitePassword && passwordSites.has(site) && (await prove(sitePassword)).proven) {
2170
2235
  provenPattern = pattern ?? provenPattern;
@@ -2180,6 +2245,14 @@ export function createTools(ctx) {
2180
2245
  return passwordConsented.add(site), true;
2181
2246
  return false;
2182
2247
  },
2248
+ makePublic: async () => !(await ask(HOST_SCREENS['netlify-visibility'])).skipped,
2249
+ retry: async (page, first) => {
2250
+ if (first)
2251
+ (ctx.openBrowser ?? openBrowser)(page);
2252
+ // The browser may not have opened (BROWSER=none, no opener): the address is on the screen as well.
2253
+ const screen = HOST_SCREENS['netlify-visibility-retry'];
2254
+ return !(await ask({ ...screen, hint: `${screen.hint} If it did not open, open this link: ${page}` })).skipped;
2255
+ },
2183
2256
  paste: async () => {
2184
2257
  const answer = await ask(HOST_SCREENS['netlify-password-key']);
2185
2258
  return answer.skipped ? undefined : answer.value?.trim();
@@ -2192,9 +2265,15 @@ export function createTools(ctx) {
2192
2265
  },
2193
2266
  remember: noteSensitiveAnswer,
2194
2267
  });
2195
- const passwordOutcome = { ...told, ...(told.status === 'saved' ? { proven: true } : {}) };
2196
- if (told.status === 'skipped' || told.status === 'login')
2268
+ const passwordOutcome = { ...told, ...(told.status === 'saved' || told.status === 'opened' ? { proven: true } : {}) };
2269
+ if (told.status === 'skipped' || told.status === 'login' || told.status === 'opened')
2197
2270
  passwordStands.set(site, passwordOutcome);
2271
+ if (told.status === 'opened') {
2272
+ // The site is public now: its previews open with nothing sent.
2273
+ hostedPreviewProven = true;
2274
+ provenHost = 'Netlify';
2275
+ provenPattern = pattern ?? provenPattern;
2276
+ }
2198
2277
  if (value) {
2199
2278
  // What is in hand follows the store: a new password is the one saved now, and the site the last one opened no longer stands.
2200
2279
  sitePassword = value;
package/dist/vercel.js CHANGED
@@ -110,7 +110,7 @@ export async function readVercelProject(vercel, link) {
110
110
  const branch = record(project.link)?.productionBranch;
111
111
  const protection = scope(project);
112
112
  const hosts = [target, ...latest].flatMap((d) => (typeof d?.url === 'string' && d.url ? [d.url.replace(/^https?:\/\//, '').toLowerCase()] : []));
113
- return { production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), protection: typeof protection === 'string' ? protection : null, deploymentHosts: new Set(hosts) };
113
+ return { ...(typeof project.name === 'string' && project.name ? { name: project.name } : {}), production: exists ? 'exists' : 'none', ...(typeof branch === 'string' && branch ? { productionBranch: branch } : {}), protection: typeof protection === 'string' ? protection : null, deploymentHosts: new Set(hosts) };
114
114
  }
115
115
  /**
116
116
  * The linked project before its first push: read through `vercel api`, and, with no production deployment, locked
@@ -118,15 +118,17 @@ export async function readVercelProject(vercel, link) {
118
118
  * branch it builds and Standard Protection serves production domains to anyone. A project that deploys production
119
119
  * already (live or building), or is locked already, is left as it is.
120
120
  */
121
- export async function prepareFirstDeployment(vercel, link) {
121
+ export async function prepareFirstDeployment(vercel, link, lock = true) {
122
122
  const project = await readVercelProject(vercel, link);
123
123
  if (!project)
124
124
  return { production: 'unknown', locked: false, reason: 'The Vercel CLI could not read the project (vercel api): it is signed out, or this CLI has no api command.' };
125
- const branch = project.productionBranch ? { productionBranch: project.productionBranch } : {};
125
+ const branch = { ...(project.productionBranch ? { productionBranch: project.productionBranch } : {}), ...(project.name ? { project: project.name } : {}) };
126
126
  if (project.production === 'exists')
127
127
  return { production: 'exists', ...branch };
128
128
  if (project.protection === 'all')
129
129
  return { production: 'none', locked: true, ...branch };
130
+ if (!lock)
131
+ return { production: 'none', locked: false, ...branch, reason: "Vercel's login is not in front of the project's addresses yet." };
130
132
  try {
131
133
  if (scope(await printedProject(vercel, lockProjectApi(link))) === 'all')
132
134
  return { production: 'none', locked: true, ...branch };
@@ -136,6 +138,28 @@ export async function prepareFirstDeployment(vercel, link) {
136
138
  }
137
139
  return { production: 'none', locked: false, ...branch, reason: "Vercel did not put its login in front of the project's addresses: that takes the Member role or Project Administrator." };
138
140
  }
141
+ /**
142
+ * A page of the project in Vercel's dashboard: by its team and name when the run knows both, else the dashboard's link
143
+ * that has the person pick them.
144
+ */
145
+ export function vercelProjectPage(page, team, project) {
146
+ return team && project ? `https://vercel.com/${encodeURIComponent(team)}/${encodeURIComponent(project)}/${page}` : `https://vercel.com/d?to=${encodeURIComponent(`/[team]/[project]/${page}`)}`;
147
+ }
148
+ /** Reads the project again every `wait.everyMs` until Vercel's login stands in front of every address of it, or it deploys production, or `wait.totalMs` has passed (a read that failed counts as neither): the person is locking it meanwhile. */
149
+ export async function waitForVercelLock(vercel, link, deps) {
150
+ for (const end = Date.now() + deps.wait.totalMs;;) {
151
+ const first = await prepareFirstDeployment(vercel, link, false);
152
+ // A read that failed says nothing yet, so the wait goes on.
153
+ if (first.production === 'exists' || first.locked || Date.now() >= end || deps.signal?.aborted)
154
+ return first;
155
+ try {
156
+ await (deps.sleep ?? ((ms) => sleep(ms, undefined, { signal: deps.signal })))(deps.wait.everyMs);
157
+ }
158
+ catch {
159
+ return first;
160
+ }
161
+ }
162
+ }
139
163
  /**
140
164
  * The hosts of the linked project's deployments while Vercel's login stands in front of every address of it, read
141
165
  * now: a Production deployment GitHub lists for a pushed branch is its preview only when it is one of these. Empty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Set up a repository for Impeccable for PRs from your terminal.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,
@@ -1,21 +0,0 @@
1
- ### When this applies
2
-
3
- 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.
4
-
5
- ### Steps
6
-
7
- 1. **Work out the commands** in the workflow's order: install, prepare, build, seed and start, each with the folder it runs in. Done when each has a command, or none, and a folder.
8
- 2. **Prove them** with impeccable_verify_preview in server mode. Done when it answers `verified: true`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof.
9
- 3. **Save them** with impeccable_write to `preview`: `intendedMethod: "ci-tunnel"` and the settings you proved, from the keys `buildSettings` lists. Done when the answer's `keys` show each key stored or unchanged.
10
-
11
- ### Reference
12
-
13
- - **Seed and migrate only a throwaway database**, never a shared or production one:
14
- - **SQLite**: a relative file URL in `env` (`DATABASE_URL=file:./preview.db`), and the repository's schema setup as the prepare or seed step.
15
- - **Postgres or MySQL**: `service`, which gives the steps their own `DATABASE_URL`.
16
- - **Cloudflare D1**: `--local` migrations and seeds.
17
- - **The tunnel host refused** (localhost answers, the tunnel host gets a 403): allow the preview host in the framework's host check (Rails `config.hosts`, Django `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS`, Vite `preview.allowedHosts`) for previews only, in the setup pull request; keep the check on.
18
- - **Readiness**: the job waits for `/` to answer. When only a base path answers, report it as a blocker: no setting points it elsewhere.
19
- - **Runtime**: compare `node --version` (or python, ruby, pnpm) with the repository's pins; the job sets its runtime from `runtimeVersion`, so a mismatch goes under "What is not verified".
20
- - **Env**: literal, non-secret values. A secret the app needs is `${{ secrets.NAME }}` in the saved `env`, and "Add the NAME repository secret" in `left`. A demo or test-login flag belongs in the preview's env only.
21
- - A verified result covers HTTP answers only: rendering and the CI runtime go under "What is not verified".