@impeccable/reviews 0.1.29 → 0.1.30

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.
@@ -66,6 +66,42 @@ function workerPreview(script, url, alias, name) {
66
66
  }
67
67
  /** What a check run's conclusion, or the words the app's table says, tell of the build. */
68
68
  const stateOf = (value) => (typeof value !== 'string' ? 'building' : /succe/i.test(value) ? 'success' : /fail|timed_out|error/i.test(value) ? 'failure' : /cancel/i.test(value) ? 'canceled' : 'building');
69
+ /** When GitHub says a record last changed, as a number to sort by; 0 where it does not say. */
70
+ const when = (value) => (typeof value === 'string' && Number.isFinite(Date.parse(value)) ? Date.parse(value) : 0);
71
+ /**
72
+ * GitHub's record, read from the setup state's hosting record (hosting.ts) instead of GitHub: Cloudflare's check runs
73
+ * and its bot's comments as Impeccable's copy holds them, a Workers Builds check's Worker among them; null where the
74
+ * record lists none for the app, so GitHub is read here.
75
+ */
76
+ export function cloudflareFromHosting(app) {
77
+ const previews = [];
78
+ const heads = [];
79
+ let worker;
80
+ for (const p of app.previews) {
81
+ if (p.source !== 'check-run' && p.source !== 'comment')
82
+ continue;
83
+ const from = p.source === 'check-run' ? 'check run' : 'bot comment';
84
+ const state = p.state === 'ready' || p.state === 'success' ? 'success' : p.state === 'failed' || p.state === 'failure' ? 'failure' : p.state === 'canceled' ? 'canceled' : 'building';
85
+ const at = when(p.at);
86
+ const head = { ...(p.pull !== null ? { pullRequest: p.pull } : {}), sha: p.sha };
87
+ if (p.site && (p.alias ?? p.url)?.endsWith('.workers.dev')) {
88
+ // A Worker's: the record's Worker and alias stand for the pattern, as a check run's summary does.
89
+ if (!worker || at > worker.at)
90
+ worker = { script: p.site, build: state, at };
91
+ const preview = workerPreview(p.site, p.url ?? undefined, p.alias ?? p.url ?? undefined);
92
+ if (preview)
93
+ previews.push({ ...head, ...preview, state, from, at });
94
+ }
95
+ else if (p.url && PAGES_ADDRESS.test(p.url)) {
96
+ previews.push({ ...head, url: p.url, ...(p.alias && PAGES_ADDRESS.test(p.alias) ? { alias: p.alias } : {}), state, from, at });
97
+ }
98
+ else
99
+ continue;
100
+ if (!heads.some((h) => h.sha === p.sha))
101
+ heads.push({ ...(p.pull !== null ? { number: p.pull } : {}), sha: p.sha, at });
102
+ }
103
+ return previews.length ? { previews, heads, failed: false, ...(worker ? { worker: { script: worker.script, build: worker.build } } : {}) } : null;
104
+ }
69
105
  /**
70
106
  * GitHub's record: the Cloudflare check run on each head (or, with `only`, that pull request's; with `branch`, the
71
107
  * pushed branch's head alone, before its pull request exists), and the app's comment on each pull request when it
@@ -77,7 +113,6 @@ async function fromGitHub(owner, repo, gh, only, branch, pattern) {
77
113
  const get = async (route) => JSON.parse(await gh(['api', '--hostname', 'github.com', '--method', 'GET', route]));
78
114
  let failed = false;
79
115
  const read = async (what, none) => what.catch(() => ((failed = true), none));
80
- const when = (value) => (typeof value === 'string' && Number.isFinite(Date.parse(value)) ? Date.parse(value) : 0);
81
116
  const heads = [];
82
117
  if (branch !== undefined) {
83
118
  const ref = record(await read(get(`${base}/branches/${encodeURIComponent(branch)}`), null));
@@ -471,7 +506,7 @@ export async function readCloudflarePreviews(owner, repo, cwd, deps = {}) {
471
506
  return unread();
472
507
  const gh = deps.gh ?? commandRunner('gh', cwd, { signal: deps.signal });
473
508
  const plain = deps.pull === undefined && deps.branch === undefined;
474
- const github = await fromGitHub(owner, repo, gh, deps.pull, deps.branch, deps.pattern);
509
+ const github = deps.github && plain ? { ...deps.github, previews: [...deps.github.previews] } : await fromGitHub(owner, repo, gh, deps.pull, deps.branch, deps.pattern);
475
510
  const config = await wranglerConfig(cwd);
476
511
  const readme = plain ? await readmeAddresses(cwd) : { pages: [] };
477
512
  // For one app's folder, an address or the root's config names no app's project: only the project's settings do.
package/dist/hosted.js CHANGED
@@ -136,6 +136,13 @@ export function liveShaped(url) {
136
136
  return LIVE.render.test(parsed.hostname);
137
137
  return false;
138
138
  }
139
+ /** The host a live app's address is on, where the address is a live app's on one of them (`liveShaped`); null otherwise. */
140
+ export function liveHost(url) {
141
+ if (!liveShaped(url))
142
+ return null;
143
+ const host = liveHostOf(new URL(url));
144
+ return host === 'other' ? null : host;
145
+ }
139
146
  /** The host a preview address belongs to, by name; null for an address of none. */
140
147
  export function previewHostName(url) {
141
148
  const host = URL.canParse(url) ? hostOf(new URL(url)) : null;
@@ -0,0 +1,76 @@
1
+ import { liveHost, renderSiteOf } from './hosted.js';
2
+ import { parseRenderDeployment } from './render.js';
3
+ const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
4
+ const text = (value) => (typeof value === 'string' && value.length > 0 && value.length <= 300 ? value : null);
5
+ /** An https address, as the record's origin and path, with no slash after it; null for anything else. */
6
+ const address = (value) => {
7
+ if (typeof value !== 'string' || value.length > 2048 || !/^https:\/\/[^\s\\?#@]+$/.test(value) || !URL.canParse(value))
8
+ return null;
9
+ const url = new URL(value);
10
+ return `${url.origin}${url.pathname}`.replace(/\/$/, '');
11
+ };
12
+ const SOURCES = new Set(['deployment', 'commit-status', 'check-run', 'comment']);
13
+ /** The record the state carries for `repo`, as far as it is well formed; null where the state has none (an older setup API). */
14
+ export function hostingOf(state, repo) {
15
+ const repos = record(state)?.repos;
16
+ const own = Array.isArray(repos) ? repos.map(record).find((r) => r?.name === repo) : undefined;
17
+ const hosting = record(record(record(own?.items)?.preview)?.hosting);
18
+ if (!hosting || !Array.isArray(hosting.apps))
19
+ return null;
20
+ const apps = hosting.apps.map(record).flatMap((a) => {
21
+ const key = text(a?.key);
22
+ if (!a || !key)
23
+ return [];
24
+ const previews = (Array.isArray(a.previews) ? a.previews : []).map(record).flatMap((p) => {
25
+ const sha = typeof p?.sha === 'string' && /^[0-9a-f]{40}$/.test(p.sha) ? p.sha : null;
26
+ const source = typeof p?.source === 'string' && SOURCES.has(p.source) ? p.source : null;
27
+ if (!p || !sha || !source)
28
+ return [];
29
+ const pull = typeof p.pull === 'number' && Number.isSafeInteger(p.pull) && p.pull > 0 ? p.pull : null;
30
+ return [{ pull, sha, url: address(p.url), alias: address(p.alias), state: text(p.state), at: text(p.at), source, environment: text(p.environment), site: text(p.site) }];
31
+ });
32
+ return [{ key, liveUrl: address(a.liveUrl), previews }];
33
+ });
34
+ return { apps };
35
+ }
36
+ /** The record's entry for an app: the one named, or, on a repository of one app, its only one. */
37
+ export function hostingApp(hosting, appKey) {
38
+ if (!hosting)
39
+ return undefined;
40
+ return appKey !== undefined ? hosting.apps.find((a) => a.key === appKey) : hosting.apps.length === 1 ? hosting.apps[0] : undefined;
41
+ }
42
+ const STATES = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
43
+ /** A Worker's own address on workers.dev, or a preview's alias: one label joined to the Worker's name, then the account's subdomain. The shape tells them apart no better than the record's previews do. */
44
+ const WORKERS_ADDRESS = /^https:\/\/[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.workers\.dev$/;
45
+ /**
46
+ * The host the record's live address is on, where the proof may open it as the live app: by its domain (hosted.ts
47
+ * `liveHost`), or a Worker's own address on workers.dev where none of the record's previews is at that address, since
48
+ * a preview's alias has the same shape; null for an address elsewhere, or none.
49
+ */
50
+ export function liveHostOf(app) {
51
+ if (!app.liveUrl)
52
+ return null;
53
+ const host = liveHost(app.liveUrl);
54
+ if (host)
55
+ return host;
56
+ return WORKERS_ADDRESS.test(app.liveUrl) && !app.previews.some((p) => p.url === app.liveUrl || p.alias === app.liveUrl) ? 'cloudflare' : null;
57
+ }
58
+ /** The deployments GitHub recorded for the app, in the shape impeccable_hosted_previews reads, for Render's read (Vercel's keeps GitHub's own read, which covers more than the record: production deployments, older pull requests): so none is Vercel's here. */
59
+ export function deploymentsOf(app) {
60
+ return app.previews.filter((p) => p.source === 'deployment').map((p) => ({
61
+ sha: p.sha,
62
+ environment: p.environment,
63
+ state: STATES.find((s) => s === p.state) ?? null,
64
+ url: p.url,
65
+ vercel: false,
66
+ pullRequests: p.pull !== null ? [p.pull] : [],
67
+ }));
68
+ }
69
+ /** The Render sites the record names for the app: its deployments' (environment and address naming the same site, as parseRenderDeployment checks) and its live address's. */
70
+ export function renderSitesOf(app) {
71
+ const sites = deploymentsOf(app).flatMap((d) => parseRenderDeployment(d.environment, d.url)?.site ?? []);
72
+ const live = app.liveUrl ? renderSiteOf(app.liveUrl) : null;
73
+ if (live && live.pull === undefined)
74
+ sites.push(live.site);
75
+ return [...new Set(sites)];
76
+ }
package/dist/main.js CHANGED
@@ -7,6 +7,7 @@ import { runAgent } from './agent.js';
7
7
  import { runSkillCommand } from './skill.js';
8
8
  import { fenceExtension, guardedFileOperations, hostsAgreed, placeOf, previewRetryState } from './fence.js';
9
9
  import { accountsSaved, focusInstructions, focusState, severalApps, signInItem } from './focus.js';
10
+ import { hostingOf } from './hosting.js';
10
11
  import { requestJson } from './http.js';
11
12
  import { copyText, letTabsHear, openBrowser, progressPage, screenPrompt, setupAuth, signIn } from './login.js';
12
13
  import { checkoutJournal } from './leftovers.js';
@@ -394,6 +395,7 @@ export async function main(argv) {
394
395
  sandbox,
395
396
  pushAccess: push.outcome,
396
397
  severalApps: severalApps(state.data),
398
+ hosting: hostingOf(state.data, token.repository.name),
397
399
  ...(flags.app ? { app: flags.app } : {}),
398
400
  start: checkout.start,
399
401
  ...(flags.signedInTestDrive ? { signedInTestDrive: { accountsSaved: accountsSaved(state.data, token.repository.name) } } : {}),
package/dist/netlify.js CHANGED
@@ -4,6 +4,7 @@ import { join } from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
5
  import { promisify } from 'node:util';
6
6
  import { folderOf, netlifyPreviewsFact, pickPreviewsFact } from './host-previews.js';
7
+ import { netlifySiteOf } from './hosted.js';
7
8
  import { gitEnvironment } from './sandbox.js';
8
9
  /**
9
10
  * Netlify: the deploy previews this repository's pull requests get (from
@@ -123,6 +124,25 @@ async function fromGitHub(owner, repo, gh, only) {
123
124
  }
124
125
  return { previews: found, heads: new Map(heads.map((head) => [head.number, head.sha])), failed };
125
126
  }
127
+ /** What GitHub records, read from the setup state's hosting record (hosting.ts) instead of GitHub: Netlify's commit statuses and its bot's comments as Impeccable's copy holds them; null where the record lists none for the app, so GitHub is read here. */
128
+ export function netlifyFromHosting(app) {
129
+ const previews = [];
130
+ const heads = new Map();
131
+ // A commit status names the head and carries the build's state; a comment proves nothing of the current build, so it stands only where no status says (as from GitHub's record).
132
+ const ranked = [...app.previews.filter((p) => p.source === 'commit-status'), ...app.previews.filter((p) => p.source === 'comment')];
133
+ for (const p of ranked) {
134
+ if (p.pull === null || !p.site || !p.url || netlifySiteOf(p.url) !== p.site)
135
+ continue;
136
+ heads.set(p.pull, p.sha);
137
+ // The address is built from the pull request and the site, as from GitHub's record: never the record's own.
138
+ const url = previewAddress(p.pull, p.site);
139
+ if (!url || previews.some((q) => q.pullRequest === p.pull && q.site === p.site))
140
+ continue;
141
+ // A bot's comment posts once the preview is ready, as from GitHub's record; a status carries its own state.
142
+ previews.push(p.source === 'comment' ? { pullRequest: p.pull, site: p.site, url, state: 'ready', from: 'bot comment', at: when(p.at) } : { pullRequest: p.pull, site: p.site, url, state: STATES.find((s) => s === p.state) ?? null, from: 'commit status', at: when(p.at) });
143
+ }
144
+ return previews.length ? { previews, heads, failed: false } : null;
145
+ }
126
146
  /** A page of the person's site list, and how many pages are read at most: what `netlify sites:list` reads. */
127
147
  const SITES_PER_PAGE = 100;
128
148
  const SITE_PAGES = 10;
@@ -198,7 +218,7 @@ export async function readNetlifyPreviews(owner, repo, cwd, deps = {}) {
198
218
  if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(repo) || repo === '.' || repo === '..') {
199
219
  return { sites: [], previews: [], cli: 'missing', error: "Netlify's previews could not be read." };
200
220
  }
201
- const [read, cli] = await Promise.all([fromGitHub(owner, repo, deps.gh ?? commandRunner('gh', cwd, { signal: deps.signal }), deps.pull), fromCli(owner, repo, deps.netlify ?? commandRunner('netlify', cwd, { signal: deps.signal }), deps.pull, deps.directory)]);
221
+ const [read, cli] = await Promise.all([deps.github && deps.pull === undefined ? deps.github : fromGitHub(owner, repo, deps.gh ?? commandRunner('gh', cwd, { signal: deps.signal }), deps.pull), fromCli(owner, repo, deps.netlify ?? commandRunner('netlify', cwd, { signal: deps.signal }), deps.pull, deps.directory)]);
202
222
  // For one app's folder, GitHub's record counts only for the site the CLI read as building it: a site's name says nothing of its folder.
203
223
  const where = deps.directory ? `the folder ${deps.directory}` : "the repository's root";
204
224
  const unmatched = deps.directory === undefined ? undefined
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Cloudflare Pages deploys previews of the pull requests, or a Worker Cloudflare builds (Workers Builds) does where its branch builds make previews: impeccable_cloudflare_previews lists a built preview of a pull request (`state` success), with its address and the branch's alias (a Worker's, on workers.dev, with the `pattern` it stands for). Ask it only where the repository is set up for Cloudflare (`wrangler.toml`, `wrangler.json` or `wrangler.jsonc`, a `wrangler` or `@cloudflare/*` package, a `.pages.dev` or `.workers.dev` address in the README or the workflows), but its record decides, not the checkout: Cloudflare's GitHub app posts a check run on each commit it built and a comment on the pull request, and cf, Cloudflare's CLI, lists the project's deployments when it is signed in here. It applies as well where the checkout is set up for Cloudflare and no pull request got a preview: the answer then says why (`pullRequestPreviews`, `worker`, `githubApp`), and "No pull-request previews" below says what the run does about each cause, in that order, before any other preview is set up. Cloudflare builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
3
+ Cloudflare Pages deploys previews of the pull requests, or a Worker Cloudflare builds (Workers Builds) does where its branch builds make previews: impeccable_cloudflare_previews lists a built preview of a pull request (`state` success), with its address and the branch's alias (a Worker's, on workers.dev, with the `pattern` it stands for). Ask it only where the state's `hosting` names Cloudflare for the app, or the repository is set up for Cloudflare (`wrangler.toml`, `wrangler.json` or `wrangler.jsonc`, a `wrangler` or `@cloudflare/*` package, a `.pages.dev` or `.workers.dev` address in the README or the workflows), but its record decides, not the checkout: Cloudflare's GitHub app posts a check run on each commit it built and a comment on the pull request, and cf, Cloudflare's CLI, lists the project's deployments when it is signed in here. It applies as well where the checkout is set up for Cloudflare and no pull request got a preview: the answer then says why (`pullRequestPreviews`, `worker`, `githubApp`), and "No pull-request previews" below says what the run does about each cause, in that order, before any other preview is set up. Cloudflare builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
4
4
 
5
5
  ### Steps
6
6
 
@@ -4,7 +4,7 @@ Every run that saves a preview method, for the state's `before` item: the addres
4
4
 
5
5
  ### Steps
6
6
 
7
- 1. **Find the address**: the `production` address a host tool answered (impeccable_netlify_previews, the Netlify site's own; impeccable_cloudflare_previews, the Pages project's, `https://<project>.pages.dev`, or with no Pages project a Worker's own, `https://<worker>.<subdomain>.workers.dev`; impeccable_render_previews, the Render site's, `https://<site>.onrender.com`; impeccable_hosted_previews, the linked Vercel project's own domain where GitHub records a production deployment of it; impeccable_vercel_connect's `productionUrl` where production exists), else the live-site line the item's `proposal` read in the README, checked against the file, when it is the site's or project's own address on one of the hosts (an address elsewhere is only the host tools' to name). A project or site created in this run has no live app yet. Done when you have one address, or know there is none.
7
+ 1. **Find the address**: the `production` address a host tool answered (impeccable_netlify_previews, the Netlify site's own; impeccable_cloudflare_previews, the Pages project's, `https://<project>.pages.dev`, or with no Pages project a Worker's own, `https://<worker>.<subdomain>.workers.dev`; impeccable_render_previews, the Render site's, `https://<site>.onrender.com`; impeccable_hosted_previews, the linked Vercel project's own domain where GitHub records a production deployment of it; impeccable_vercel_connect's `productionUrl` where production exists), else the `liveUrl` the state's `hosting` names for the app (the saved before, or the address Impeccable read in the repository), else the live-site line the item's `proposal` read in the README, checked against the file, when it is the site's or project's own address on one of the hosts (an address elsewhere is only the host tools' to name). A project or site created in this run has no live app yet. Done when you have one address, or know there is none.
8
8
  2. **Prove it** with impeccable_prove_preview, under the status line *"Opening the live app"*: that address, or a page on it a visitor opens without signing in, and words the page shows that the checkout holds. A key the run already got in place (Vercel's bypass secret, Netlify's site password, Cloudflare's service token) goes with it where the lock is the same project's or site's. Done when it answers `proven: true`. **An address no key opens is not set**: a `gate`, the app's own sign-in on every page, or no answer leaves the item as it is, and the report says what the address answered.
9
9
  3. **Save it** with impeccable_write to `preview`: `beforeUrl` the proven address's root, `https://<host>` with no path, whatever page the proof opened (a login page is one role's, and the review signs in as another; impeccable_write saves the root either way), in the same write as the method where the proof came before it (`{"intendedMethod": "preview-deploy", "beforeUrl": "https://…"}`), else in a write of its own; `appKey` the app's on a repository of several apps, one address per app. Done when the answer carries `beforeUrl`; its `beforeCheck.access` says how Impeccable's own check opened it: `ok`; `auth-wall`, the app's sign-in, which the saved test accounts get past; anything else goes under "What is not verified".
10
10
 
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Netlify deploys previews of the pull requests: impeccable_netlify_previews lists a built preview of a pull request (`state` success or ready), with its address. Ask it only where the repository is set up for Netlify (`netlify.toml`, `.netlify/`, `netlify-cli` or an `@netlify/*` package), but its record decides, not the checkout. Netlify builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
3
+ Netlify deploys previews of the pull requests: impeccable_netlify_previews lists a built preview of a pull request (`state` success or ready), with its address. Ask it only where the state's `hosting` names Netlify for the app, or the repository is set up for Netlify (`netlify.toml`, `.netlify/`, `netlify-cli` or an `@netlify/*` package), but its record decides, not the checkout. Netlify builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
4
4
 
5
5
  It applies as well **before the first pull request**: a site built from this repository has Deploy Previews on (impeccable_netlify_previews lists none, and its `pullRequestPreviews` says `enabled: true`; on a repository with several apps, read with the app's `appKey`), so its deploy previews start with the test drive's draft, the site's first. Then there is nothing to prove in step 1 yet: impeccable_ask with `netlify-setup` is refused, since there is no sign-in, site or connection to make. Run impeccable_netlify_connect (with the app's `appKey`) first: it changes nothing on that site, and checks that Netlify's GitHub app still reads the repository, asking the person once for its access on GitHub when it does not (a connection left undone ends as netlify-setup's "A step fails before the test drive is pushed" says). Then go on to pull-requests' step 4, which opens the test drive as a draft, waits for its deploy preview (impeccable_netlify_previews with `pull`), proves the test drive's page there, past Netlify's lock as preview-protection says, and then step 2 saves the method.
6
6
 
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Render deploys the app: impeccable_render_previews (with the app's `appKey` on a repository with several apps) answers a `site`. Read it wherever the checkout names an `onrender.com` address or a `render.yaml`, impeccable_hosted_previews lists a deployment at an `onrender.com` address, or no other host's tool showed this app, before server-previews. Render builds and serves every preview, a site of its own for each pull request (`https://<site>-pr-<n>.onrender.com`), so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request. The tool finds the site from the render CLI, Render's deployments on GitHub and the checkout's files, and shows the person its one screen itself: **the person never types an address**, and impeccable_ask refuses a question asking for one.
3
+ Render deploys the app: impeccable_render_previews (with the app's `appKey` on a repository with several apps) answers a `site`. Read it wherever the state's `hosting` names Render for the app, the checkout names an `onrender.com` address or a `render.yaml`, impeccable_hosted_previews lists a deployment at an `onrender.com` address, or no other host's tool showed this app, before server-previews. Render builds and serves every preview, a site of its own for each pull request (`https://<site>-pr-<n>.onrender.com`), so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request. The tool finds the site from the render CLI, the state's `hosting`, Render's deployments on GitHub and the checkout's files, and shows the person its one screen itself: **the person never types an address**, and impeccable_ask refuses a question asking for one.
4
4
 
5
5
  ### Steps
6
6
 
@@ -1,6 +1,6 @@
1
1
  ### When this applies
2
2
 
3
- Vercel deploys previews of the pull requests: impeccable_hosted_previews lists a successful Preview deployment that Vercel made (`vercel: true`) of a pull request, with its address. The checkout often shows it too (`vercel.json`, `.vercel/project.json`, a `vercel` dependency), but GitHub's record decides. Vercel builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
3
+ Vercel deploys previews of the pull requests: impeccable_hosted_previews lists a successful Preview deployment that Vercel made (`vercel: true`) of a pull request, with its address. The state's `hosting` naming Vercel for the app, and the checkout (`vercel.json`, `.vercel/project.json`, a `vercel` dependency), say to read it; GitHub's record decides. Vercel builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
4
4
 
5
5
  It applies as well **before the first pull request**: Vercel deploys the repository already (impeccable_hosted_previews lists deployments Vercel made, production ones or of pushes, and none of a pull request; on a repository with several apps, those of the project that builds the app, named in `project`), so its previews start with the test drive's push. Then there is nothing to prove in step 1 yet: impeccable_ask with `vercel-setup` is refused, since there is no sign-in, project or connection to make; go on to pull-requests' step 4, which asks for Vercel's bypass secret where the project's previews are protected, pushes the test drive, waits for Vercel's deployment of the pushed branch (impeccable_hosted_previews with `branch`, and the app's `appKey`), proves the test drive's page there, past Vercel's login as preview-protection says, and then step 2 saves the method.
6
6
 
package/dist/playbook.js CHANGED
@@ -31,9 +31,9 @@ Set the repository up with whatever the host's CLI can do (Netlify's, Vercel's,
31
31
 
32
32
  # Steps
33
33
 
34
- 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\`.
34
+ 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\`, \`workflowStanding\` and \`hosting\` (each app's host, live address and the previews GitHub recorded, as Impeccable already holds them: a start, which the host tools take and the proofs still prove; absent from an older setup API), whether there is a \`signin\` item, whether there is an \`apps\` item (the monorepo case comes first then), and the titles in \`notPartOfThisSetup\`.
35
35
  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.
36
- 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, or a site of this repository with Deploy Previews on (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's (a Pages project's, or a Worker's), as the render-previews case says when impeccable_render_previews answers a Render site, 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; then the live app's address as the live-app-address case says, in the method's write where it can be. Done when those cases' steps are done.
36
+ 3. **Set up the preview**, starting with the host \`hosting\` names for the app (its case and its tool first; GitHub's record decides, so a named host whose tool lists nothing sends you on to the next): 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, or a site of this repository with Deploy Previews on (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's (a Pages project's, or a Worker's), as the render-previews case says when impeccable_render_previews answers a Render site, 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; then the live app's address as the live-app-address case says, in the method's write where it can be. Done when those cases' steps are done.
37
37
  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.
38
38
  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\`.
39
39
  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.
package/dist/tools.js CHANGED
@@ -13,10 +13,11 @@ import { appsLeftOut, focusCheckup, focusInstructions, focusState, severalApps }
13
13
  import { APPS_QUESTION, cliInstallScreen, cliManualScreen, cloudflareAccessConsent, cloudflareAccessPaste, cloudflareAccessReusable, HOST_QUESTIONS, HOST_SCREENS, HOST_SIGNED_IN, namedAppQuestion, NETLIFY_PASSWORD_WHERE, netlifyPasswordReplace, renderConfirm, sentTo, startedIn, vercelBypassConsent, vercelBypassPaste } from './host-questions.js';
14
14
  import { enableNetlifyPreviews, folderOf } from './host-previews.js';
15
15
  import { checkoutNames, liveShaped, netlifySiteOf, previewHostName, provePreview, readHostedPreviews, readmeNamesApp, renderSiteOf, vercelDeploymentOf } from './hosted.js';
16
+ import { deploymentsOf, hostingApp, liveHostOf, renderSitesOf } from './hosting.js';
16
17
  import { enableRenderPreviews, parseRenderDeployment, readRenderService, RENDER_DASHBOARD, renderSitesNamed } from './render.js';
17
- import { cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
18
+ import { cloudflareFromHosting, cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
18
19
  import { passCloudflareAccess } from './cloudflare-access.js';
19
- import { commandRunner, connectNetlifySite, liveSite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
20
+ import { netlifyFromHosting, commandRunner, connectNetlifySite, liveSite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
20
21
  import { netlifyToken, passNetlifyLock, setSitePassword } from './netlify-password.js';
21
22
  import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, projectMadeDeployment, readLinkedProject, readOwnProject, vercelDashboardScope, vercelPreviewsFact, vercelProjectFor, vercelProjectPage, vercelRepoProject, vercelStatus, vercelTeams, waitForVercelAccess, waitForVercelLock } from './vercel.js';
22
23
  import { BYPASS_HEADER, passVercelLogin, readLink } from './vercel-bypass.js';
@@ -2454,6 +2455,14 @@ export function createTools(ctx) {
2454
2455
  };
2455
2456
  /** An app's folder, read from the state the first time it is asked for. */
2456
2457
  const appFolder = async (pi, key) => (appFolders.has(key) ? appFolders.get(key) : (await stateApps(pi), appFolders.get(key)));
2458
+ /** The setup state's record of an app's hosting (hosting.ts), as the state read at the start carried it; none from an older setup API, and every host tool then reads GitHub itself. */
2459
+ const hostingFor = (appKey) => hostingApp(ctx.hosting, appKey);
2460
+ // The live address the record names is a candidate for the before, proven as a host tool's production address is, on the host its domain says.
2461
+ for (const a of ctx.hosting?.apps ?? []) {
2462
+ const host = liveHostOf(a);
2463
+ if (host)
2464
+ noteProduction(a.liveUrl, host, undefined, appsRepo ? a.key : undefined);
2465
+ }
2457
2466
  /**
2458
2467
  * On a repository with several apps, why a step for `appKey` may not run yet: the apps question comes first, and only
2459
2468
  * a picked app is set up. Null where it may: one app, or a picked one.
@@ -3264,7 +3273,7 @@ export function createTools(ctx) {
3264
3273
  noteApp(appKey, { host_project: app.name });
3265
3274
  return JSON.stringify({ ...(appKey !== undefined ? { appKey, project: app.name } : {}), ...evidence, ...(production ? { production } : {}) });
3266
3275
  }),
3267
- 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). production is the live site's own address, for the before. 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) => {
3276
+ 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 (from the setup state's hosting record where it holds them, else read from GitHub here), or the Netlify CLI on this machine when it is signed in (read only: it never signs in). production is the live site's own address, for the before. 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) => {
3268
3277
  const pull = p.pull;
3269
3278
  const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
3270
3279
  if (refused)
@@ -3276,7 +3285,10 @@ export function createTools(ctx) {
3276
3285
  return JSON.stringify({ appKey, sites: [], previews: [], unmatched: `The setup state names no folder for the app ${appKey}.` });
3277
3286
  // Each site's team is for the last screen alone.
3278
3287
  const onTeams = (teams) => Object.entries(teams).forEach(([name, team]) => accountOf.set(`netlify ${name}`, team));
3279
- const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, onTeams, ...(directory !== undefined ? { directory } : {}) });
3288
+ // GitHub's record as the setup state holds it, where it lists Netlify's previews of this app; a wait on a pull request reads GitHub, since its preview is newer than the record.
3289
+ const known = pull === undefined ? hostingFor(appKey) : undefined;
3290
+ const github = known ? netlifyFromHosting(known) : null;
3291
+ const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, onTeams, ...(directory !== undefined ? { directory } : {}), ...(github ? { github } : {}) });
3280
3292
  // A pull request's preview is still building until Netlify's deploy of it is built or has failed.
3281
3293
  const states = ['success', 'ready', 'failure', 'error'];
3282
3294
  const final = (evidence) => Boolean(evidence.error) || evidence.previews.some((preview) => states.includes(preview.state ?? ''));
@@ -3313,7 +3325,7 @@ export function createTools(ctx) {
3313
3325
  noteApp(appKey, { host_project: site });
3314
3326
  return JSON.stringify(appKey !== undefined ? { appKey, ...evidence } : evidence);
3315
3327
  }),
3316
- 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; where cf is not installed here, the person is asked to install it first, on its own screen). production is the project's own address, https://<project>.pages.dev, the live app, for the before. 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. pullRequestPreviews says whether the Pages project built from this repository builds previews of pull requests at all (enabled false, with the cause: source, deployments or previews); worker says Cloudflare builds a Worker from the repository (Workers Builds); where its branch builds make previews, they are listed too, on workers.dev (the alias https://<branch>-<worker>.<subdomain>.workers.dev, with the pattern it stands for), and with no Pages project production is the Worker's own address; githubApp says what gh reads of Cloudflare's GitHub app on the owner, and the page where it gets access. 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; where the project's settings build every branch and no build came, the wait's screen opens the GitHub page where Cloudflare's app gets access to the repository instead, and waits again. It changes nothing on Cloudflare and proves nothing about the page.", CLOUDFLARE_PREVIEWS, async (p, pi, signal) => {
3328
+ 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 (from the setup state's hosting record where it holds them, else read from GitHub here), or cf, Cloudflare's CLI, on this machine when it is signed in (read only: it never signs in; where cf is not installed here, the person is asked to install it first, on its own screen). production is the project's own address, https://<project>.pages.dev, the live app, for the before. 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. pullRequestPreviews says whether the Pages project built from this repository builds previews of pull requests at all (enabled false, with the cause: source, deployments or previews); worker says Cloudflare builds a Worker from the repository (Workers Builds); where its branch builds make previews, they are listed too, on workers.dev (the alias https://<branch>-<worker>.<subdomain>.workers.dev, with the pattern it stands for), and with no Pages project production is the Worker's own address; githubApp says what gh reads of Cloudflare's GitHub app on the owner, and the page where it gets access. 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; where the project's settings build every branch and no build came, the wait's screen opens the GitHub page where Cloudflare's app gets access to the repository instead, and waits again. It changes nothing on Cloudflare and proves nothing about the page.", CLOUDFLARE_PREVIEWS, async (p, pi, signal) => {
3317
3329
  const pull = p.pull;
3318
3330
  const branch = p.branch;
3319
3331
  if (pull !== undefined && branch !== undefined)
@@ -3333,7 +3345,10 @@ export function createTools(ctx) {
3333
3345
  let accountName;
3334
3346
  // A Worker Preview's build names no address: its alias is made from the pattern the plain read's Worker preview stood for.
3335
3347
  const pattern = cloudflareEvidence?.previews.find((preview) => preview.pattern)?.pattern;
3336
- const readAll = () => (ctx.cloudflarePreviewReader ?? readCloudflarePreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, branch, onAccountName: (name) => (accountName = name), ...(directory !== undefined && pull === undefined && branch === undefined ? { directory } : {}), ...(appProject ? { project: appProject.project } : {}), ...(pattern ? { pattern } : {}) });
3348
+ // GitHub's record as the setup state holds it, where it lists Cloudflare's previews of this app; a wait on a pull request or a pushed branch reads GitHub, since its build is newer than the record.
3349
+ const known = pull === undefined && branch === undefined ? hostingFor(appKey) : undefined;
3350
+ const github = known ? cloudflareFromHosting(known) : null;
3351
+ const readAll = () => (ctx.cloudflarePreviewReader ?? readCloudflarePreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, branch, onAccountName: (name) => (accountName = name), ...(directory !== undefined && pull === undefined && branch === undefined ? { directory } : {}), ...(appProject ? { project: appProject.project } : {}), ...(pattern ? { pattern } : {}), ...(github ? { github } : {}) });
3337
3352
  const read = async () => {
3338
3353
  const evidence = await readAll();
3339
3354
  return appProject ? { ...evidence, previews: evidence.previews.filter((preview) => pagesProject(preview.url) === appProject.label) } : evidence;
@@ -3391,7 +3406,7 @@ export function createTools(ctx) {
3391
3406
  noteProduction(evidence.unmatched ? undefined : evidence.production, 'cloudflare', undefined, appKey);
3392
3407
  return JSON.stringify(appKey !== undefined ? { appKey, ...evidence } : evidence);
3393
3408
  }),
3394
- tool('impeccable_prove_preview', 'Check a hosted preview', 'Open a preview address impeccable_hosted_previews, impeccable_netlify_previews, impeccable_cloudflare_previews or impeccable_render_previews listed for a pull request or a pushed branch (a successful Preview deployment, a deploy preview that is ready, a Pages build that succeeded, or a Render preview), or a page on it, as a visitor signed in nowhere, and prove it: it answers, and the page shows words a file of this checkout holds. proven true lets impeccable_write save intendedMethod "preview-deploy"; for Netlify, Cloudflare (Pages, or a Worker\'s previews) and Render it answers the address pattern to save beside it. gate "vercel-protection", "netlify-protection" or "cloudflare-access" means the host\'s protection stands in front of it. Or open the live app\'s address, the production one of those tools answered or one a file of this checkout names (the README\'s live-site line), or a page on it, the same way: proven true lets impeccable_write save it as beforeUrl, and nothing else. A key the run got in place for the same project or site goes with it. Nothing of the page comes back.', PROVE_PREVIEW, async (p, pi, signal) => {
3409
+ tool('impeccable_prove_preview', 'Check a hosted preview', 'Open a preview address impeccable_hosted_previews, impeccable_netlify_previews, impeccable_cloudflare_previews or impeccable_render_previews listed for a pull request or a pushed branch (a successful Preview deployment, a deploy preview that is ready, a Pages build that succeeded, or a Render preview), or a page on it, as a visitor signed in nowhere, and prove it: it answers, and the page shows words a file of this checkout holds. proven true lets impeccable_write save intendedMethod "preview-deploy"; for Netlify, Cloudflare (Pages, or a Worker\'s previews) and Render it answers the address pattern to save beside it. gate "vercel-protection", "netlify-protection" or "cloudflare-access" means the host\'s protection stands in front of it. Or open the live app\'s address, the production one of those tools answered, the one the setup state\'s hosting record names for the app (liveUrl) or one a file of this checkout names (the README\'s live-site line), or a page on it, the same way: proven true lets impeccable_write save it as beforeUrl, and nothing else. A key the run got in place for the same project or site goes with it. Nothing of the page comes back.', PROVE_PREVIEW, async (p, pi, signal) => {
3395
3410
  const url = p.url;
3396
3411
  const origin = URL.canParse(url) ? new URL(url).origin : null;
3397
3412
  if (origin && publicProduction.has(origin))
@@ -3401,7 +3416,7 @@ export function createTools(ctx) {
3401
3416
  const known = origin !== null ? productionListed.get(origin) : undefined;
3402
3417
  const live = origin !== null && !listed && (known !== undefined || (liveShaped(url) && (await checkoutNames(pi.cwd, origin))));
3403
3418
  if (!origin || (!listed && !live))
3404
- return "Refused: open the address of a preview that impeccable_hosted_previews, impeccable_netlify_previews, impeccable_cloudflare_previews or impeccable_render_previews listed for a pull request or a pushed branch, or a page on it; or, for the before, the live app's address one of them answered as production, or the site's or project's own address on one of the hosts that a file of this checkout names.";
3419
+ return "Refused: open the address of a preview that impeccable_hosted_previews, impeccable_netlify_previews, impeccable_cloudflare_previews or impeccable_render_previews listed for a pull request or a pushed branch, or a page on it; or, for the before, the live app's address one of them answered as production, the one the setup state's hosting record names for the app, or the site's or project's own address on one of the hosts that a file of this checkout names.";
3405
3420
  // The Netlify site, by the address's shape or, for the site's custom domain, as its tool named it: its password goes to the site's own addresses.
3406
3421
  const site = netlifySiteOf(url) ?? known?.site;
3407
3422
  // A saved service token opens its Pages project's previews, or its Worker's: Access admits it wherever the application's policy covers.
@@ -3481,7 +3496,7 @@ export function createTools(ctx) {
3481
3496
  return JSON.stringify({ ...proof, appKey: app.appKey });
3482
3497
  return JSON.stringify(proof);
3483
3498
  }),
3484
- tool('impeccable_render_previews', 'Render previews', "Read whether Render builds this repository's pull requests, on a repository with several apps for the app's own folder (appKey): the Render site, from the render CLI signed in on this machine (the service built from this repository, in the app's folder by its Root Directory), else the one site Render's deployments on GitHub and the checkout's files name (on a repository with several apps, a file in the app's own folder, or a line of the root README naming that app and no other); its previews as GitHub records them, each with its pull request, its address and its state (a closed pull request's address too, read from its earlier successful status, though Render took that preview down); production, the site's own address, for the before; pattern, the address every pull request's preview has; and pullRequestPreviews (enabled false, with the cause, where the service's previews are off or manual; 'unknown' without the CLI's answer). cli says whether the render CLI answered (signed-in), is missing, could not list the services (unread, with the reason), or lists none of this repository's (none). Once a site is found, it shows the person one screen naming Render, with the site's address to click, asked once for each app: confirmed true, or false when the person skipped it, which is a no that ends the setup for that app. Where the CLI says the previews are off, that screen is also the consent to turning them on, and on the yes the CLI turns them on and reads them back, with a retry screen on Render's page where it could not: turnedOn in the answer says how that ended (enabled, by run or person; skipped; refused, with the reason and the page). With pull (a pull request of this run, a draft included: Render builds the preview as soon as a pull request opens), only that pull request's preview of the site the plain read found, waiting up to five minutes for it to finish building, under a status line with its timer, and again on the person's yes when the time ran out. Never asks for an address.", RENDER_PREVIEWS, async (p, pi, signal) => {
3499
+ tool('impeccable_render_previews', 'Render previews', "Read whether Render builds this repository's pull requests, on a repository with several apps for the app's own folder (appKey): the Render site, from the render CLI signed in on this machine (the service built from this repository, in the app's folder by its Root Directory), else the one site the setup state's hosting record names for the app (Render's deployments, its live address), else the one Render's deployments on GitHub and the checkout's files name (on a repository with several apps, a file in the app's own folder, or a line of the root README naming that app and no other); its previews as GitHub records them (from the state's record where it holds them, else read from GitHub here), each with its pull request, its address and its state (a closed pull request's address too, read from its earlier successful status, though Render took that preview down); production, the site's own address, for the before; pattern, the address every pull request's preview has; and pullRequestPreviews (enabled false, with the cause, where the service's previews are off or manual; 'unknown' without the CLI's answer). cli says whether the render CLI answered (signed-in), is missing, could not list the services (unread, with the reason), or lists none of this repository's (none). Once a site is found, it shows the person one screen naming Render, with the site's address to click, asked once for each app: confirmed true, or false when the person skipped it, which is a no that ends the setup for that app. Where the CLI says the previews are off, that screen is also the consent to turning them on, and on the yes the CLI turns them on and reads them back, with a retry screen on Render's page where it could not: turnedOn in the answer says how that ended (enabled, by run or person; skipped; refused, with the reason and the page). With pull (a pull request of this run, a draft included: Render builds the preview as soon as a pull request opens), only that pull request's preview of the site the plain read found, waiting up to five minutes for it to finish building, under a status line with its timer, and again on the person's yes when the time ran out. Never asks for an address.", RENDER_PREVIEWS, async (p, pi, signal) => {
3485
3500
  const pull = p.pull;
3486
3501
  const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
3487
3502
  if (refused)
@@ -3530,9 +3545,16 @@ export function createTools(ctx) {
3530
3545
  const render = (ctx.hostRunner ?? commandRunner)('render', pi.cwd, { signal });
3531
3546
  const read = await readRenderService(render, repository, folder);
3532
3547
  const service = 'service' in read ? read.service : undefined;
3533
- const evidence = await readEvidence();
3548
+ // GitHub's record as the setup state holds it, where it lists Render's deployments of this app; else read here.
3549
+ const record = hostingFor(appKey);
3550
+ const recorded = record ? deploymentsOf(record).filter((d) => parseRenderDeployment(d.environment, d.url)) : [];
3551
+ const evidence = recorded.length ? { deployments: recorded } : await readEvidence();
3534
3552
  let site = service?.site;
3535
- // Without the CLI's answer, the site the repository names: on a repository of one app, the one Render site its deployments and files name; on one of several, the one a file in the app's own folder names, else the one a line of the root README names beside that app and no other.
3553
+ // Without the CLI's answer, the one site the setup state's record names for the app (its deployments, its live address); else the one the repository names: on a repository of one app, the one Render site its deployments and files name; on one of several, the one a file in the app's own folder names, else the one a line of the root README names beside that app and no other.
3554
+ if (!site && record) {
3555
+ const named = renderSitesOf(record);
3556
+ site = named.length === 1 ? named[0] : undefined;
3557
+ }
3536
3558
  if (!site) {
3537
3559
  const recorded = evidence.deployments.flatMap((d) => parseRenderDeployment(d.environment, d.url)?.site ?? []);
3538
3560
  if (appKey === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
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,