@impeccable/reviews 0.1.29 → 0.1.31
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 +92 -0
- package/dist/log.js +24 -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 +2 -2
- package/dist/playbook/cases/vercel-previews.md +1 -1
- package/dist/playbook.js +2 -2
- package/dist/tools.js +81 -36
- 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 and is not `read` (hosting.ts `hostingRead`), so GitHub is read here.
|
|
75
|
+
*/
|
|
76
|
+
export function cloudflareFromHosting(app, read = false) {
|
|
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 || read ? { 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,92 @@
|
|
|
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
|
+
const pullState = p.pullState === 'open' || p.pullState === 'closed' ? p.pullState : null;
|
|
31
|
+
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), pullState }];
|
|
32
|
+
});
|
|
33
|
+
return [{ key, liveUrl: address(a.liveUrl), previews }];
|
|
34
|
+
});
|
|
35
|
+
const unread = Array.isArray(hosting.unread) ? hosting.unread.flatMap((u) => text(u) ?? []) : [];
|
|
36
|
+
return { apps, readAt: text(hosting.readAt), unread };
|
|
37
|
+
}
|
|
38
|
+
/** The reports each host's previews are read from, besides the deployments every read takes. */
|
|
39
|
+
const REPORTS = { netlify: ['statuses', 'comments'], cloudflare: ['check-runs', 'comments'], render: [] };
|
|
40
|
+
/**
|
|
41
|
+
* Whether the record is the whole of GitHub's record of the host's earlier previews, an empty list included: Impeccable
|
|
42
|
+
* read the repository's copy (`readAt`), and took every report the host's previews are read from. Else a host tool
|
|
43
|
+
* reads GitHub itself where the record lists none.
|
|
44
|
+
*/
|
|
45
|
+
export function hostingRead(hosting, host) {
|
|
46
|
+
if (!hosting?.readAt)
|
|
47
|
+
return false;
|
|
48
|
+
return !REPORTS[host].some((report) => hosting.unread.includes(report));
|
|
49
|
+
}
|
|
50
|
+
/** The pull requests whose previews the app's record lists as closed: on Render, evidence of the site and the pattern, never a preview to prove. */
|
|
51
|
+
export const closedPulls = (app) => new Set(app?.previews.flatMap((p) => (p.pullState === 'closed' && p.pull !== null ? [p.pull] : [])) ?? []);
|
|
52
|
+
/** The record's entry for an app: the one named, or, on a repository of one app, its only one. */
|
|
53
|
+
export function hostingApp(hosting, appKey) {
|
|
54
|
+
if (!hosting)
|
|
55
|
+
return undefined;
|
|
56
|
+
return appKey !== undefined ? hosting.apps.find((a) => a.key === appKey) : hosting.apps.length === 1 ? hosting.apps[0] : undefined;
|
|
57
|
+
}
|
|
58
|
+
const STATES = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
|
|
59
|
+
/** 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. */
|
|
60
|
+
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$/;
|
|
61
|
+
/**
|
|
62
|
+
* The host the record's live address is on, where the proof may open it as the live app: by its domain (hosted.ts
|
|
63
|
+
* `liveHost`), or a Worker's own address on workers.dev where none of the record's previews is at that address, since
|
|
64
|
+
* a preview's alias has the same shape; null for an address elsewhere, or none.
|
|
65
|
+
*/
|
|
66
|
+
export function liveHostOf(app) {
|
|
67
|
+
if (!app.liveUrl)
|
|
68
|
+
return null;
|
|
69
|
+
const host = liveHost(app.liveUrl);
|
|
70
|
+
if (host)
|
|
71
|
+
return host;
|
|
72
|
+
return WORKERS_ADDRESS.test(app.liveUrl) && !app.previews.some((p) => p.url === app.liveUrl || p.alias === app.liveUrl) ? 'cloudflare' : null;
|
|
73
|
+
}
|
|
74
|
+
/** 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. */
|
|
75
|
+
export function deploymentsOf(app) {
|
|
76
|
+
return app.previews.filter((p) => p.source === 'deployment').map((p) => ({
|
|
77
|
+
sha: p.sha,
|
|
78
|
+
environment: p.environment,
|
|
79
|
+
state: STATES.find((s) => s === p.state) ?? null,
|
|
80
|
+
url: p.url,
|
|
81
|
+
vercel: false,
|
|
82
|
+
pullRequests: p.pull !== null ? [p.pull] : [],
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
/** 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. */
|
|
86
|
+
export function renderSitesOf(app) {
|
|
87
|
+
const sites = deploymentsOf(app).flatMap((d) => parseRenderDeployment(d.environment, d.url)?.site ?? []);
|
|
88
|
+
const live = app.liveUrl ? renderSiteOf(app.liveUrl) : null;
|
|
89
|
+
if (live && live.pull === undefined)
|
|
90
|
+
sites.push(live.site);
|
|
91
|
+
return [...new Set(sites)];
|
|
92
|
+
}
|
package/dist/log.js
CHANGED
|
@@ -26,6 +26,30 @@ export function redact(text, secrets = []) {
|
|
|
26
26
|
export function scrub(text, secrets = [], field) {
|
|
27
27
|
return redactShapes(redact(text, secrets), field);
|
|
28
28
|
}
|
|
29
|
+
/** Flags whose next argument is a value of no interest to the log: gh's host, cf's profile. */
|
|
30
|
+
const VALUED = new Set(['--hostname', '--profile']);
|
|
31
|
+
/**
|
|
32
|
+
* A host CLI's call as one debug-log line, for counting the reads a run made: the command, its subcommand words, gh
|
|
33
|
+
* api's method and an API route without its query. It stops at the first argument that is none of those, so no flag's
|
|
34
|
+
* value (a `--data` body, a password, a token) is ever written.
|
|
35
|
+
*/
|
|
36
|
+
export function commandLine(name, args) {
|
|
37
|
+
const parts = [name];
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (arg === '--method' && /^[A-Z]{3,7}$/.test(args[i + 1] ?? ''))
|
|
41
|
+
parts.push(args[++i]);
|
|
42
|
+
else if (VALUED.has(arg))
|
|
43
|
+
i++;
|
|
44
|
+
else if (/^[A-Za-z][A-Za-z:-]{0,39}$/.test(arg))
|
|
45
|
+
parts.push(arg);
|
|
46
|
+
else if (/^\/[\w./%~-]*(?:\?.*)?$/.test(arg))
|
|
47
|
+
parts.push(arg.split('?')[0]);
|
|
48
|
+
else
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
return parts.join(' ');
|
|
52
|
+
}
|
|
29
53
|
export function createLog(debug, dir = tmpdir()) {
|
|
30
54
|
const secrets = new Set();
|
|
31
55
|
const secret = (value) => void secrets.add(value);
|
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 and is not `read` (hosting.ts `hostingRead`), so GitHub is read here. */
|
|
128
|
+
export function netlifyFromHosting(app, read = false) {
|
|
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 || read ? { 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
|
|
|
@@ -8,7 +8,7 @@ Render deploys the app: impeccable_render_previews (with the app's `appKey` on a
|
|
|
8
8
|
- **`confirmed: false`**: the person skipped the Render screen, a no that ends the setup for the app, as the playbook says.
|
|
9
9
|
- **`turnedOn`** says how turning the service's previews on ended, where the CLI said they were off and the screen was the consent: `enabled`, go on; `skipped` or `refused`, push no test drive (pull-requests' "No preview, no test drive"), and say under "What was skipped, and why" that Render's pull request previews are off for the site, with the `reason`, and name the `page`; `left` stays empty, since the person was asked in this run.
|
|
10
10
|
- **No `site`**: Render's previews do not show this app. Go on as server-previews says, with `unread` under "What is not verified" where the CLI did not answer.
|
|
11
|
-
2. **Prove the newest preview** with impeccable_prove_preview: the address of the newest listed preview whose state is `success` (or a page on it a visitor opens without signing in), and words the page shows that the checkout holds. Done when it answers `proven: true`; when an address no longer answers
|
|
11
|
+
2. **Prove the newest preview** with impeccable_prove_preview: the address of the newest listed preview whose state is `success` (or a page on it a visitor opens without signing in), and words the page shows that the checkout holds; a closed pull request's preview, `pullState: "closed"`, names the site and the pattern only, and counts as none listed, since Render takes it down. Done when it answers `proven: true`; when an address no longer answers, try the next newest, three at most. With none listed, or none that answers, the test drive's own preview is the proof: go on to pull-requests' step 4.
|
|
12
12
|
3. **Save the method** with impeccable_write to `preview`: `{"previewUrlPattern": "<the proof's pattern>", "intendedMethod": "preview-deploy"}`, with the app's `appKey` in a monorepo, since each app's site has a pattern of its own. Done when the answer's `keys` show both stored or unchanged.
|
|
13
13
|
|
|
14
14
|
### Reference
|
|
@@ -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,15 +13,16 @@ 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 { closedPulls, deploymentsOf, hostingApp, hostingRead, 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';
|
|
23
24
|
import { fitsTask, stagesOf, workStarted } from './stages.js';
|
|
24
|
-
import { redact } from './log.js';
|
|
25
|
+
import { commandLine, redact } from './log.js';
|
|
25
26
|
import { scan } from './scan.js';
|
|
26
27
|
import { gitEnvironment, installEnvironment, ownFolder } from './sandbox.js';
|
|
27
28
|
import { openBrowser } from './login.js';
|
|
@@ -1625,6 +1626,24 @@ export function createWorkflowSynchronizer(opts = {}) {
|
|
|
1625
1626
|
}
|
|
1626
1627
|
export function createTools(ctx) {
|
|
1627
1628
|
const text = (t, terminate = false) => ({ content: [{ type: 'text', text: t }], details: undefined, ...(terminate ? { terminate } : {}) });
|
|
1629
|
+
/** A host CLI (gh, render, netlify, cf, vercel, npm) as the host tools run it: each call one line in the debug log, its command and route alone (log.ts `commandLine`), so a run's reads can be counted. */
|
|
1630
|
+
const hostRunner = (name, cwd, opts) => {
|
|
1631
|
+
// Made on its first call: a reader handed one it never calls runs nothing.
|
|
1632
|
+
let run;
|
|
1633
|
+
return async (args) => {
|
|
1634
|
+
const line = `host call: ${commandLine(name, args)}`;
|
|
1635
|
+
try {
|
|
1636
|
+
run ??= (ctx.hostRunner ?? commandRunner)(name, cwd, opts);
|
|
1637
|
+
const out = await run(args);
|
|
1638
|
+
ctx.log.write(line);
|
|
1639
|
+
return out;
|
|
1640
|
+
}
|
|
1641
|
+
catch (err) {
|
|
1642
|
+
ctx.log.write(`${line} (failed)`);
|
|
1643
|
+
throw err;
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
};
|
|
1628
1647
|
const retry = previewRetryState(ctx.log);
|
|
1629
1648
|
retry.repository ??= `${ctx.owner}/${ctx.repo}`;
|
|
1630
1649
|
// Once the list is up: the agent's first call sets it, with the reading in progress.
|
|
@@ -1681,6 +1700,8 @@ export function createTools(ctx) {
|
|
|
1681
1700
|
const workerPatterns = new Map();
|
|
1682
1701
|
/** The origins of the successful Render previews impeccable_render_previews listed, of the app's own site. */
|
|
1683
1702
|
const renderListed = new Set();
|
|
1703
|
+
/** Of those, the ones built in this run, which its wait with `pull` listed: the only ones whose 404 is asked for again, since Render may not serve their build yet. */
|
|
1704
|
+
const renderBuilt = new Set();
|
|
1684
1705
|
let hostedPreviewProven = false;
|
|
1685
1706
|
/**
|
|
1686
1707
|
* The address pattern each proof answered, by the app it was proven for, and the last one under '' for a repository
|
|
@@ -1981,7 +2002,7 @@ export function createTools(ctx) {
|
|
|
1981
2002
|
liveApps.set(new URL(production).origin, appKey);
|
|
1982
2003
|
};
|
|
1983
2004
|
const hostCli = (host, pi, signal) => {
|
|
1984
|
-
const runner = (browser = false) =>
|
|
2005
|
+
const runner = (browser = false) => hostRunner(host, pi.cwd, { signal, browser, timeoutMs: browser ? 300_000 : 30_000 });
|
|
1985
2006
|
const cli = runner();
|
|
1986
2007
|
if (host === 'vercel') {
|
|
1987
2008
|
return {
|
|
@@ -2081,7 +2102,7 @@ export function createTools(ctx) {
|
|
|
2081
2102
|
/** The run's own install of a host CLI's package, for installHostCli: npm's pinned global install, from the command's own folder and npm cache, as the agent's install of one runs; whether it completed. */
|
|
2082
2103
|
const npmInstall = (pkg, signal) => async () => {
|
|
2083
2104
|
try {
|
|
2084
|
-
await
|
|
2105
|
+
await hostRunner('npm', ownFolder(), { signal, timeoutMs: 600_000, env: installEnvironment() })(hostCliInstallArgs(pkg));
|
|
2085
2106
|
return true;
|
|
2086
2107
|
}
|
|
2087
2108
|
catch {
|
|
@@ -2089,7 +2110,7 @@ export function createTools(ctx) {
|
|
|
2089
2110
|
}
|
|
2090
2111
|
};
|
|
2091
2112
|
/** cf, Cloudflare's CLI, which only the tools run: where it is missing here, its install is asked first (#396). */
|
|
2092
|
-
const cfPresent = async (pi, signal) => (await cfStatus(cfRunner({ checkout: pi.cwd },
|
|
2113
|
+
const cfPresent = async (pi, signal) => (await cfStatus(cfRunner({ checkout: pi.cwd }, hostRunner), signal)).cli !== 'missing';
|
|
2093
2114
|
const installCf = (pi, signal) => installHostCli('cloudflare', () => cfPresent(pi, signal), npmInstall('cf', signal), signal);
|
|
2094
2115
|
/**
|
|
2095
2116
|
* A host's sign-in, on `impeccable_ask` with the id `<host>-setup`: the setup screen once, worded for a CLI signed in
|
|
@@ -2193,8 +2214,8 @@ export function createTools(ctx) {
|
|
|
2193
2214
|
// The site's settings change on the host: only once the person opened the Netlify setup, as the fence holds for the CLI. A site built from this repository with Deploy Previews on needs no setup (#311): the connection changes nothing on it, and only checks that Netlify's GitHub app still reads the repository, asking for its access when it does not.
|
|
2194
2215
|
if (!hostSetupAgreed(ctx.log, 'netlify') && !previewing)
|
|
2195
2216
|
return { connected: false, reason: 'The Netlify sign-in comes first: impeccable_ask with the id netlify-setup.' };
|
|
2196
|
-
const netlify =
|
|
2197
|
-
const gh =
|
|
2217
|
+
const netlify = hostRunner('netlify', pi.cwd, { signal });
|
|
2218
|
+
const gh = hostRunner('gh', pi.cwd, { signal });
|
|
2198
2219
|
const clone = ctx.hostWait?.clone;
|
|
2199
2220
|
const result = await connectNetlifySite(ctx.owner, ctx.repo, pi.cwd, { netlify, gh, linked: linkedByRun(ctx.log), settings, signal, ...(clone ? { wait: clone } : {}), ...(directory !== undefined ? { directory } : {}), ...(previewing ? { ownSite: true } : {}) });
|
|
2200
2221
|
netlifySite = result.site?.name ?? netlifySite;
|
|
@@ -2220,7 +2241,7 @@ export function createTools(ctx) {
|
|
|
2220
2241
|
async function vercelConnect(pi, signal) {
|
|
2221
2242
|
if (!hostSetupAgreed(ctx.log, 'vercel'))
|
|
2222
2243
|
return { connected: false, reason: 'The Vercel sign-in comes first: impeccable_ask with the id vercel-setup.' };
|
|
2223
|
-
const vercel =
|
|
2244
|
+
const vercel = hostRunner('vercel', pi.cwd, { signal });
|
|
2224
2245
|
const link = await readLink(pi.cwd);
|
|
2225
2246
|
if (!link)
|
|
2226
2247
|
return { connected: false, reason: 'This folder is linked to no Vercel project (.vercel/project.json): run vercel link first.' };
|
|
@@ -2454,6 +2475,14 @@ export function createTools(ctx) {
|
|
|
2454
2475
|
};
|
|
2455
2476
|
/** An app's folder, read from the state the first time it is asked for. */
|
|
2456
2477
|
const appFolder = async (pi, key) => (appFolders.has(key) ? appFolders.get(key) : (await stateApps(pi), appFolders.get(key)));
|
|
2478
|
+
/** 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. */
|
|
2479
|
+
const hostingFor = (appKey) => hostingApp(ctx.hosting, appKey);
|
|
2480
|
+
// 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.
|
|
2481
|
+
for (const a of ctx.hosting?.apps ?? []) {
|
|
2482
|
+
const host = liveHostOf(a);
|
|
2483
|
+
if (host)
|
|
2484
|
+
noteProduction(a.liveUrl, host, undefined, appsRepo ? a.key : undefined);
|
|
2485
|
+
}
|
|
2457
2486
|
/**
|
|
2458
2487
|
* On a repository with several apps, why a step for `appKey` may not run yet: the apps question comes first, and only
|
|
2459
2488
|
* a picked app is set up. Null where it may: one app, or a picked one.
|
|
@@ -3170,7 +3199,7 @@ export function createTools(ctx) {
|
|
|
3170
3199
|
const vercel = (args) => {
|
|
3171
3200
|
const key = JSON.stringify(args);
|
|
3172
3201
|
if (!reads.has(key))
|
|
3173
|
-
reads.set(key,
|
|
3202
|
+
reads.set(key, hostRunner('vercel', pi.cwd, { signal })(args));
|
|
3174
3203
|
return reads.get(key);
|
|
3175
3204
|
};
|
|
3176
3205
|
for (const origin of listedPreviews)
|
|
@@ -3183,7 +3212,7 @@ export function createTools(ctx) {
|
|
|
3183
3212
|
const app = appKey !== undefined ? await vercelApp(pi, appKey, vercel) : undefined;
|
|
3184
3213
|
if (app && 'reason' in app)
|
|
3185
3214
|
return JSON.stringify({ appKey, deployments: [], unmatched: app.reason });
|
|
3186
|
-
const raw = () => (ctx.hostedPreviewReader ?? readHostedPreviews)(ctx.owner, ctx.repo, { signal, branch });
|
|
3215
|
+
const raw = () => (ctx.hostedPreviewReader ?? readHostedPreviews)(ctx.owner, ctx.repo, { signal, branch, runGh: hostRunner('gh', pi.cwd, { signal }) });
|
|
3187
3216
|
// Vercel names each project's deployments on GitHub "Preview – <project>"; one without a name is told by the project's own record of it.
|
|
3188
3217
|
const owned = async (d) => {
|
|
3189
3218
|
if (!app)
|
|
@@ -3264,7 +3293,7 @@ export function createTools(ctx) {
|
|
|
3264
3293
|
noteApp(appKey, { host_project: app.name });
|
|
3265
3294
|
return JSON.stringify({ ...(appKey !== undefined ? { appKey, project: app.name } : {}), ...evidence, ...(production ? { production } : {}) });
|
|
3266
3295
|
}),
|
|
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) => {
|
|
3296
|
+
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 lists them or carries readAt, 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
3297
|
const pull = p.pull;
|
|
3269
3298
|
const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
|
|
3270
3299
|
if (refused)
|
|
@@ -3276,7 +3305,10 @@ export function createTools(ctx) {
|
|
|
3276
3305
|
return JSON.stringify({ appKey, sites: [], previews: [], unmatched: `The setup state names no folder for the app ${appKey}.` });
|
|
3277
3306
|
// Each site's team is for the last screen alone.
|
|
3278
3307
|
const onTeams = (teams) => Object.entries(teams).forEach(([name, team]) => accountOf.set(`netlify ${name}`, team));
|
|
3279
|
-
|
|
3308
|
+
// GitHub's record as the setup state holds it, where it lists Netlify's previews of this app, or none on a record Impeccable read; a wait on a pull request reads GitHub, since its preview is newer than the record.
|
|
3309
|
+
const known = pull === undefined ? hostingFor(appKey) : undefined;
|
|
3310
|
+
const github = known ? netlifyFromHosting(known, hostingRead(ctx.hosting, 'netlify')) : null;
|
|
3311
|
+
const read = () => (ctx.netlifyPreviewReader ?? readNetlifyPreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, onTeams, gh: hostRunner('gh', pi.cwd, { signal }), netlify: hostRunner('netlify', pi.cwd, { signal }), ...(directory !== undefined ? { directory } : {}), ...(github ? { github } : {}) });
|
|
3280
3312
|
// A pull request's preview is still building until Netlify's deploy of it is built or has failed.
|
|
3281
3313
|
const states = ['success', 'ready', 'failure', 'error'];
|
|
3282
3314
|
const final = (evidence) => Boolean(evidence.error) || evidence.previews.some((preview) => states.includes(preview.state ?? ''));
|
|
@@ -3313,7 +3345,7 @@ export function createTools(ctx) {
|
|
|
3313
3345
|
noteApp(appKey, { host_project: site });
|
|
3314
3346
|
return JSON.stringify(appKey !== undefined ? { appKey, ...evidence } : evidence);
|
|
3315
3347
|
}),
|
|
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) => {
|
|
3348
|
+
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 lists them or carries readAt, 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
3349
|
const pull = p.pull;
|
|
3318
3350
|
const branch = p.branch;
|
|
3319
3351
|
if (pull !== undefined && branch !== undefined)
|
|
@@ -3333,7 +3365,10 @@ export function createTools(ctx) {
|
|
|
3333
3365
|
let accountName;
|
|
3334
3366
|
// A Worker Preview's build names no address: its alias is made from the pattern the plain read's Worker preview stood for.
|
|
3335
3367
|
const pattern = cloudflareEvidence?.previews.find((preview) => preview.pattern)?.pattern;
|
|
3336
|
-
|
|
3368
|
+
// GitHub's record as the setup state holds it, where it lists Cloudflare's previews of this app, or none on a record Impeccable read; a wait on a pull request or a pushed branch reads GitHub, since its build is newer than the record.
|
|
3369
|
+
const known = pull === undefined && branch === undefined ? hostingFor(appKey) : undefined;
|
|
3370
|
+
const github = known ? cloudflareFromHosting(known, hostingRead(ctx.hosting, 'cloudflare')) : null;
|
|
3371
|
+
const readAll = () => (ctx.cloudflarePreviewReader ?? readCloudflarePreviews)(ctx.owner, ctx.repo, pi.cwd, { signal, pull, branch, gh: hostRunner('gh', pi.cwd, { signal }), run: hostRunner, onAccountName: (name) => (accountName = name), ...(directory !== undefined && pull === undefined && branch === undefined ? { directory } : {}), ...(appProject ? { project: appProject.project } : {}), ...(pattern ? { pattern } : {}), ...(github ? { github } : {}) });
|
|
3337
3372
|
const read = async () => {
|
|
3338
3373
|
const evidence = await readAll();
|
|
3339
3374
|
return appProject ? { ...evidence, previews: evidence.previews.filter((preview) => pagesProject(preview.url) === appProject.label) } : evidence;
|
|
@@ -3391,7 +3426,7 @@ export function createTools(ctx) {
|
|
|
3391
3426
|
noteProduction(evidence.unmatched ? undefined : evidence.production, 'cloudflare', undefined, appKey);
|
|
3392
3427
|
return JSON.stringify(appKey !== undefined ? { appKey, ...evidence } : evidence);
|
|
3393
3428
|
}),
|
|
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) => {
|
|
3429
|
+
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
3430
|
const url = p.url;
|
|
3396
3431
|
const origin = URL.canParse(url) ? new URL(url).origin : null;
|
|
3397
3432
|
if (origin && publicProduction.has(origin))
|
|
@@ -3401,7 +3436,7 @@ export function createTools(ctx) {
|
|
|
3401
3436
|
const known = origin !== null ? productionListed.get(origin) : undefined;
|
|
3402
3437
|
const live = origin !== null && !listed && (known !== undefined || (liveShaped(url) && (await checkoutNames(pi.cwd, origin))));
|
|
3403
3438
|
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.";
|
|
3439
|
+
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
3440
|
// 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
3441
|
const site = netlifySiteOf(url) ?? known?.site;
|
|
3407
3442
|
// A saved service token opens its Pages project's previews, or its Worker's: Access admits it wherever the application's policy covers.
|
|
@@ -3415,8 +3450,8 @@ export function createTools(ctx) {
|
|
|
3415
3450
|
...(token ? { access: token } : {}),
|
|
3416
3451
|
});
|
|
3417
3452
|
let proved = await prove();
|
|
3418
|
-
// Render reports a preview's deploy successful before its edge serves that build (REN-1117): a page the push added answers 404 for a while, so it is asked for again here, within a bounded wait, the way a command's sleep would but the fence refuses.
|
|
3419
|
-
if (!live && renderListed.has(origin) && proved.status === 404) {
|
|
3453
|
+
// Render reports a preview's deploy successful before its edge serves that build (REN-1117): a page the push added answers 404 for a while, so it is asked for again here, within a bounded wait, the way a command's sleep would but the fence refuses. Only a build of this run's: an earlier preview's 404 is a preview Render took down.
|
|
3454
|
+
if (!live && renderListed.has(origin) && renderBuilt.has(origin) && proved.status === 404) {
|
|
3420
3455
|
const wait = ctx.hostWait?.serve ?? { totalMs: 90_000, everyMs: 5_000 };
|
|
3421
3456
|
said('Waiting for Render to serve the new page');
|
|
3422
3457
|
for (const end = Date.now() + wait.totalMs; proved.status === 404 && Date.now() < end && !signal?.aborted;) {
|
|
@@ -3481,7 +3516,7 @@ export function createTools(ctx) {
|
|
|
3481
3516
|
return JSON.stringify({ ...proof, appKey: app.appKey });
|
|
3482
3517
|
return JSON.stringify(proof);
|
|
3483
3518
|
}),
|
|
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) => {
|
|
3519
|
+
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 lists them or carries readAt, 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; pullState closed marks one the record knows is closed, and impeccable_prove_preview refuses it); 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
3520
|
const pull = p.pull;
|
|
3486
3521
|
const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
|
|
3487
3522
|
if (refused)
|
|
@@ -3496,18 +3531,19 @@ export function createTools(ctx) {
|
|
|
3496
3531
|
if (pull !== undefined && !known)
|
|
3497
3532
|
return `Refused: read impeccable_render_previews${appKey !== undefined ? ` with appKey ${appKey}` : ''} and no pull first: it tells which Render site builds ${appKey !== undefined ? 'that app' : 'this repository'}.`;
|
|
3498
3533
|
// GitHub's record of the repository's deployments (with `branch`, its head's), and of those Render's previews of `site`, of `pull` alone where it is given.
|
|
3499
|
-
const readEvidence = (branch) => (ctx.hostedPreviewReader ?? readHostedPreviews)(ctx.owner, ctx.repo, { signal, ...(branch !== undefined ? { branch } : {}) });
|
|
3534
|
+
const readEvidence = (branch) => (ctx.hostedPreviewReader ?? readHostedPreviews)(ctx.owner, ctx.repo, { signal, runGh: hostRunner('gh', pi.cwd, { signal }), ...(branch !== undefined ? { branch } : {}) });
|
|
3500
3535
|
const previewsOf = (evidence, site) => evidence.deployments.flatMap((d) => {
|
|
3501
3536
|
const preview = parseRenderDeployment(d.environment, d.url);
|
|
3502
3537
|
return preview && preview.site === site && (pull === undefined || preview.pull === pull) ? [{ pull: preview.pull, url: d.url, state: d.state }] : [];
|
|
3503
3538
|
});
|
|
3504
|
-
// The successful previews are the ones a proof may open, each the app's.
|
|
3539
|
+
// The successful previews are the ones a proof may open, each the app's, except a closed pull request's (hosting.ts `closedPulls`): Render takes it down when the pull request closes, so it is evidence of the site and the pattern alone, marked `pullState: "closed"`.
|
|
3540
|
+
const closed = closedPulls(hostingFor(appKey));
|
|
3505
3541
|
const list = (previews) => {
|
|
3506
3542
|
for (const origin of renderListed)
|
|
3507
3543
|
listedApps.delete(origin);
|
|
3508
3544
|
renderListed.clear();
|
|
3509
3545
|
for (const preview of previews) {
|
|
3510
|
-
if (preview.state !== 'success')
|
|
3546
|
+
if (preview.state !== 'success' || closed.has(preview.pull))
|
|
3511
3547
|
continue;
|
|
3512
3548
|
renderListed.add(new URL(preview.url).origin);
|
|
3513
3549
|
if (appKey !== undefined)
|
|
@@ -3525,14 +3561,23 @@ export function createTools(ctx) {
|
|
|
3525
3561
|
const final = (e) => Boolean(e.error) || e.previews.some((preview) => preview.state === 'success' || preview.state === 'failure' || preview.state === 'error');
|
|
3526
3562
|
const waited = await awaitBuild('Render', `Waiting for Render to build ${await subjectOf(pull)}`, read, final, () => known.page, signal);
|
|
3527
3563
|
list(waited.previews);
|
|
3564
|
+
for (const origin of renderListed)
|
|
3565
|
+
renderBuilt.add(origin);
|
|
3528
3566
|
return JSON.stringify({ ...forApp, site: known.site, ...waited });
|
|
3529
3567
|
}
|
|
3530
|
-
const render =
|
|
3568
|
+
const render = hostRunner('render', pi.cwd, { signal });
|
|
3531
3569
|
const read = await readRenderService(render, repository, folder);
|
|
3532
3570
|
const service = 'service' in read ? read.service : undefined;
|
|
3533
|
-
|
|
3571
|
+
// GitHub's record as the setup state holds it, where it lists Render's deployments of this app, or none on a record Impeccable read; else read here.
|
|
3572
|
+
const record = hostingFor(appKey);
|
|
3573
|
+
const recorded = record ? deploymentsOf(record).filter((d) => parseRenderDeployment(d.environment, d.url)) : [];
|
|
3574
|
+
const evidence = recorded.length || (record && hostingRead(ctx.hosting, 'render')) ? { deployments: recorded } : await readEvidence();
|
|
3534
3575
|
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.
|
|
3576
|
+
// 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.
|
|
3577
|
+
if (!site && record) {
|
|
3578
|
+
const named = renderSitesOf(record);
|
|
3579
|
+
site = named.length === 1 ? named[0] : undefined;
|
|
3580
|
+
}
|
|
3536
3581
|
if (!site) {
|
|
3537
3582
|
const recorded = evidence.deployments.flatMap((d) => parseRenderDeployment(d.environment, d.url)?.site ?? []);
|
|
3538
3583
|
if (appKey === undefined) {
|
|
@@ -3552,7 +3597,7 @@ export function createTools(ctx) {
|
|
|
3552
3597
|
const unread = 'reason' in read ? { unread: read.reason } : {};
|
|
3553
3598
|
if (!site)
|
|
3554
3599
|
return JSON.stringify({ ...forApp, previews: [], cli, ...unread, ...(evidence.error ? { error: evidence.error } : {}) });
|
|
3555
|
-
const previews = previewsOf(evidence, site);
|
|
3600
|
+
const previews = previewsOf(evidence, site).map((p) => (closed.has(p.pull) ? { ...p, pullState: 'closed' } : p));
|
|
3556
3601
|
list(previews);
|
|
3557
3602
|
const production = service?.production ?? `https://${site}.onrender.com`;
|
|
3558
3603
|
noteProduction(production, 'render', undefined, appKey);
|
|
@@ -3630,8 +3675,8 @@ export function createTools(ctx) {
|
|
|
3630
3675
|
// cf is installed here: the fact this acts on is one a read through cf found.
|
|
3631
3676
|
const { status, ...more } = await turnOnPreviews(host, async () => {
|
|
3632
3677
|
if (host === 'netlify')
|
|
3633
|
-
return enableNetlifyPreviews(
|
|
3634
|
-
const cf = cfRunner({ checkout: pi.cwd },
|
|
3678
|
+
return enableNetlifyPreviews(hostRunner('netlify', pi.cwd, { signal }), fact.project);
|
|
3679
|
+
const cf = cfRunner({ checkout: pi.cwd }, hostRunner);
|
|
3635
3680
|
return enableCloudflarePreviews(cf, fact.project, await cfStatus(cf, signal), signal);
|
|
3636
3681
|
}, signal);
|
|
3637
3682
|
return answer(status, more);
|
|
@@ -3665,8 +3710,8 @@ export function createTools(ctx) {
|
|
|
3665
3710
|
if ((await ask({ ...screen, prompt: create ? `Create the Pages project ${project} for ${ctx.owner}/${ctx.repo}?` : `Connect ${ctx.owner}/${ctx.repo} to the Pages project ${project}?` })).skipped)
|
|
3666
3711
|
return answer('skipped', { reason: `The person skipped ${create ? 'creating the Pages project' : 'connecting the repository to the Pages project'}.` });
|
|
3667
3712
|
// cf signed in with pages:write, as it is or after Cloudflare's sign-in in the browser, keeping the scopes it held; a sign-in that did not complete offers a retry, twice.
|
|
3668
|
-
const cf = cfRunner({ checkout: pi.cwd },
|
|
3669
|
-
const login = cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd },
|
|
3713
|
+
const cf = cfRunner({ checkout: pi.cwd }, hostRunner);
|
|
3714
|
+
const login = cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, hostRunner);
|
|
3670
3715
|
const writes = (status) => status.cli === 'signed-in' && (!status.scopes || status.scopes.includes('pages:write'));
|
|
3671
3716
|
let status = await cfStatus(cf, signal);
|
|
3672
3717
|
// cf not on this machine (#396): its install is asked first.
|
|
@@ -3696,7 +3741,7 @@ export function createTools(ctx) {
|
|
|
3696
3741
|
}
|
|
3697
3742
|
}
|
|
3698
3743
|
said(create ? `Creating the Pages project ${project}` : `Connecting the repository to ${project}`);
|
|
3699
|
-
const gh =
|
|
3744
|
+
const gh = hostRunner('gh', pi.cwd, { signal });
|
|
3700
3745
|
const outcome = await connectCloudflareProject(cf, gh, status, { owner: ctx.owner, repo: ctx.repo }, { project, ...(evidence.account ? { account: evidence.account } : {}), ...(create ? { create: { command: p.command, outputDir: p.outputDir } } : {}) }, signal);
|
|
3701
3746
|
if (outcome.status !== 'connected')
|
|
3702
3747
|
return answer('refused', { reason: outcome.reason });
|
|
@@ -3735,7 +3780,7 @@ export function createTools(ctx) {
|
|
|
3735
3780
|
said("Getting past Vercel's login");
|
|
3736
3781
|
if (at?.app)
|
|
3737
3782
|
lockPending.delete(at.app.appKey);
|
|
3738
|
-
const vercel =
|
|
3783
|
+
const vercel = hostRunner('vercel', pi.cwd, { signal });
|
|
3739
3784
|
// Before the push, the app's project as impeccable_hosted_previews reads it: the one that builds the app's folder (#281).
|
|
3740
3785
|
let app = at?.app;
|
|
3741
3786
|
if (before && appsRepo) {
|
|
@@ -3970,7 +4015,7 @@ export function createTools(ctx) {
|
|
|
3970
4015
|
/** Netlify took a new password the run set in this call: the end screen says so, saved or not, and on a cancel too. */
|
|
3971
4016
|
let replaced = false;
|
|
3972
4017
|
let answer;
|
|
3973
|
-
const netlify =
|
|
4018
|
+
const netlify = hostRunner('netlify', pi.cwd, { signal });
|
|
3974
4019
|
// The Netlify CLI not on this machine (#396): its install is asked first, the run's own since no agent's turn comes in between, and the command runs again.
|
|
3975
4020
|
const run = async (args) => {
|
|
3976
4021
|
try {
|
|
@@ -4135,8 +4180,8 @@ export function createTools(ctx) {
|
|
|
4135
4180
|
accessConsented.add(found.id);
|
|
4136
4181
|
return yes;
|
|
4137
4182
|
},
|
|
4138
|
-
cf: cfRunner({ checkout: pi.cwd },
|
|
4139
|
-
login: cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd },
|
|
4183
|
+
cf: cfRunner({ checkout: pi.cwd }, hostRunner),
|
|
4184
|
+
login: cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, hostRunner),
|
|
4140
4185
|
retry: async () => !(await ask(HOST_SCREENS['cloudflare-signin-retry'])).skipped,
|
|
4141
4186
|
install: async () => void (await installCf(pi, signal)),
|
|
4142
4187
|
status: said,
|