@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.
- package/dist/cloudflare.js +37 -2
- package/dist/hosted.js +7 -0
- package/dist/hosting.js +76 -0
- package/dist/main.js +2 -0
- package/dist/netlify.js +21 -1
- package/dist/playbook/cases/cloudflare-previews.md +1 -1
- package/dist/playbook/cases/live-app-address.md +1 -1
- package/dist/playbook/cases/netlify-previews.md +1 -1
- package/dist/playbook/cases/render-previews.md +1 -1
- package/dist/playbook/cases/vercel-previews.md +1 -1
- package/dist/playbook.js +2 -2
- package/dist/tools.js +33 -11
- package/package.json +1 -1
package/dist/cloudflare.js
CHANGED
|
@@ -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;
|
package/dist/hosting.js
ADDED
|
@@ -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
|
|
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 \`
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|