@impeccable/reviews 0.1.30 → 0.1.32
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 +3 -3
- package/dist/hosting.js +35 -2
- package/dist/log.js +24 -0
- package/dist/main.js +4 -2
- package/dist/netlify.js +3 -3
- package/dist/playbook/cases/render-previews.md +1 -1
- package/dist/tools.js +60 -37
- package/package.json +1 -1
package/dist/cloudflare.js
CHANGED
|
@@ -71,9 +71,9 @@ const when = (value) => (typeof value === 'string' && Number.isFinite(Date.parse
|
|
|
71
71
|
/**
|
|
72
72
|
* GitHub's record, read from the setup state's hosting record (hosting.ts) instead of GitHub: Cloudflare's check runs
|
|
73
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.
|
|
74
|
+
* record lists none for the app and is not `read` (hosting.ts `hostingRead`), so GitHub is read here.
|
|
75
75
|
*/
|
|
76
|
-
export function cloudflareFromHosting(app) {
|
|
76
|
+
export function cloudflareFromHosting(app, read = false) {
|
|
77
77
|
const previews = [];
|
|
78
78
|
const heads = [];
|
|
79
79
|
let worker;
|
|
@@ -100,7 +100,7 @@ export function cloudflareFromHosting(app) {
|
|
|
100
100
|
if (!heads.some((h) => h.sha === p.sha))
|
|
101
101
|
heads.push({ ...(p.pull !== null ? { number: p.pull } : {}), sha: p.sha, at });
|
|
102
102
|
}
|
|
103
|
-
return previews.length ? { previews, heads, failed: false, ...(worker ? { worker: { script: worker.script, build: worker.build } } : {}) } : null;
|
|
103
|
+
return previews.length || read ? { previews, heads, failed: false, ...(worker ? { worker: { script: worker.script, build: worker.build } } : {}) } : null;
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* GitHub's record: the Cloudflare check run on each head (or, with `only`, that pull request's; with `branch`, the
|
package/dist/hosting.js
CHANGED
|
@@ -27,12 +27,45 @@ export function hostingOf(state, repo) {
|
|
|
27
27
|
if (!p || !sha || !source)
|
|
28
28
|
return [];
|
|
29
29
|
const pull = typeof p.pull === 'number' && Number.isSafeInteger(p.pull) && p.pull > 0 ? p.pull : null;
|
|
30
|
-
|
|
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 }];
|
|
31
32
|
});
|
|
32
33
|
return [{ key, liveUrl: address(a.liveUrl), previews }];
|
|
33
34
|
});
|
|
34
|
-
|
|
35
|
+
const unread = Array.isArray(hosting.unread) ? hosting.unread.flatMap((u) => text(u) ?? []) : [];
|
|
36
|
+
return { apps, readAt: text(hosting.readAt), unread, ...(hosting.readPending === true ? { readPending: true } : {}) };
|
|
35
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The record once it is whole: while the state says a read of it is pending, the state is read again every few seconds,
|
|
40
|
+
* up to a minute by the clock, each read bounded by what is left of it. Past that, its previews are left out and it
|
|
41
|
+
* counts as never read, so each host tool reads its previews itself, as it does without a record; the live addresses stay.
|
|
42
|
+
*/
|
|
43
|
+
export async function settledHosting(first, repo, readState, opts = {}) {
|
|
44
|
+
const { everyMs = 3_000, upToMs = 60_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), now = Date.now } = opts;
|
|
45
|
+
const deadline = now() + upToMs;
|
|
46
|
+
let hosting = hostingOf(first, repo);
|
|
47
|
+
while (hosting?.readPending && now() + everyMs < deadline) {
|
|
48
|
+
await sleep(everyMs);
|
|
49
|
+
const state = await readState(deadline - now());
|
|
50
|
+
if (state.ok)
|
|
51
|
+
hosting = hostingOf(state.data, repo) ?? hosting;
|
|
52
|
+
}
|
|
53
|
+
return hosting?.readPending ? { apps: hosting.apps.map((a) => ({ ...a, previews: [] })), readAt: null, unread: hosting.unread } : hosting;
|
|
54
|
+
}
|
|
55
|
+
/** The reports each host's previews are read from, besides the deployments every read takes. */
|
|
56
|
+
const REPORTS = { netlify: ['statuses', 'comments'], cloudflare: ['check-runs', 'comments'], render: [] };
|
|
57
|
+
/**
|
|
58
|
+
* Whether the record is the whole of GitHub's record of the host's earlier previews, an empty list included: Impeccable
|
|
59
|
+
* read the repository's copy (`readAt`), and took every report the host's previews are read from. Else a host tool
|
|
60
|
+
* reads GitHub itself where the record lists none.
|
|
61
|
+
*/
|
|
62
|
+
export function hostingRead(hosting, host) {
|
|
63
|
+
if (!hosting?.readAt)
|
|
64
|
+
return false;
|
|
65
|
+
return !REPORTS[host].some((report) => hosting.unread.includes(report));
|
|
66
|
+
}
|
|
67
|
+
/** 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. */
|
|
68
|
+
export const closedPulls = (app) => new Set(app?.previews.flatMap((p) => (p.pullState === 'closed' && p.pull !== null ? [p.pull] : [])) ?? []);
|
|
36
69
|
/** The record's entry for an app: the one named, or, on a repository of one app, its only one. */
|
|
37
70
|
export function hostingApp(hosting, appKey) {
|
|
38
71
|
if (!hosting)
|
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,7 +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 {
|
|
10
|
+
import { settledHosting } from './hosting.js';
|
|
11
11
|
import { requestJson } from './http.js';
|
|
12
12
|
import { copyText, letTabsHear, openBrowser, progressPage, screenPrompt, setupAuth, signIn } from './login.js';
|
|
13
13
|
import { checkoutJournal } from './leftovers.js';
|
|
@@ -234,6 +234,8 @@ export async function main(argv) {
|
|
|
234
234
|
]);
|
|
235
235
|
if (!instructions.ok || !state.ok)
|
|
236
236
|
throw new UserError(`Impeccable's setup API did not answer: ${instructions.error ?? state.error}.`);
|
|
237
|
+
// A hosting record behind GitHub is read again while the questions below are asked (hosting.ts `settledHosting`).
|
|
238
|
+
const hosting = settledHosting(state.data, token.repository.name, (timeoutMs) => requestJson(`${setupApi}/state`, { token: token.access_token, timeoutMs }, log));
|
|
237
239
|
// When the checkout cannot tell, the setup state's own verdict decides. When it has a web front end Impeccable skipped, the person adds it first.
|
|
238
240
|
if (stateSaysNoFrontEnd(state.data, token.repository.name)) {
|
|
239
241
|
if (frontEnd === 'unknown')
|
|
@@ -395,7 +397,7 @@ export async function main(argv) {
|
|
|
395
397
|
sandbox,
|
|
396
398
|
pushAccess: push.outcome,
|
|
397
399
|
severalApps: severalApps(state.data),
|
|
398
|
-
hosting:
|
|
400
|
+
hosting: await hosting,
|
|
399
401
|
...(flags.app ? { app: flags.app } : {}),
|
|
400
402
|
start: checkout.start,
|
|
401
403
|
...(flags.signedInTestDrive ? { signedInTestDrive: { accountsSaved: accountsSaved(state.data, token.repository.name) } } : {}),
|
package/dist/netlify.js
CHANGED
|
@@ -124,8 +124,8 @@ async function fromGitHub(owner, repo, gh, only) {
|
|
|
124
124
|
}
|
|
125
125
|
return { previews: found, heads: new Map(heads.map((head) => [head.number, head.sha])), failed };
|
|
126
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) {
|
|
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
129
|
const previews = [];
|
|
130
130
|
const heads = new Map();
|
|
131
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).
|
|
@@ -141,7 +141,7 @@ export function netlifyFromHosting(app) {
|
|
|
141
141
|
// A bot's comment posts once the preview is ready, as from GitHub's record; a status carries its own state.
|
|
142
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
143
|
}
|
|
144
|
-
return previews.length ? { previews, heads, failed: false } : null;
|
|
144
|
+
return previews.length || read ? { previews, heads, failed: false } : null;
|
|
145
145
|
}
|
|
146
146
|
/** A page of the person's site list, and how many pages are read at most: what `netlify sites:list` reads. */
|
|
147
147
|
const SITES_PER_PAGE = 100;
|
|
@@ -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
|
package/dist/tools.js
CHANGED
|
@@ -13,7 +13,7 @@ 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
|
+
import { closedPulls, deploymentsOf, hostingApp, hostingRead, liveHostOf, renderSitesOf } from './hosting.js';
|
|
17
17
|
import { enableRenderPreviews, parseRenderDeployment, readRenderService, RENDER_DASHBOARD, renderSitesNamed } from './render.js';
|
|
18
18
|
import { cloudflareFromHosting, cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
|
|
19
19
|
import { passCloudflareAccess } from './cloudflare-access.js';
|
|
@@ -22,7 +22,7 @@ import { netlifyToken, passNetlifyLock, setSitePassword } from './netlify-passwo
|
|
|
22
22
|
import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, projectMadeDeployment, readLinkedProject, readOwnProject, vercelDashboardScope, vercelPreviewsFact, vercelProjectFor, vercelProjectPage, vercelRepoProject, vercelStatus, vercelTeams, waitForVercelAccess, waitForVercelLock } from './vercel.js';
|
|
23
23
|
import { BYPASS_HEADER, passVercelLogin, readLink } from './vercel-bypass.js';
|
|
24
24
|
import { fitsTask, stagesOf, workStarted } from './stages.js';
|
|
25
|
-
import { redact } from './log.js';
|
|
25
|
+
import { commandLine, redact } from './log.js';
|
|
26
26
|
import { scan } from './scan.js';
|
|
27
27
|
import { gitEnvironment, installEnvironment, ownFolder } from './sandbox.js';
|
|
28
28
|
import { openBrowser } from './login.js';
|
|
@@ -1626,6 +1626,24 @@ export function createWorkflowSynchronizer(opts = {}) {
|
|
|
1626
1626
|
}
|
|
1627
1627
|
export function createTools(ctx) {
|
|
1628
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
|
+
};
|
|
1629
1647
|
const retry = previewRetryState(ctx.log);
|
|
1630
1648
|
retry.repository ??= `${ctx.owner}/${ctx.repo}`;
|
|
1631
1649
|
// Once the list is up: the agent's first call sets it, with the reading in progress.
|
|
@@ -1682,6 +1700,8 @@ export function createTools(ctx) {
|
|
|
1682
1700
|
const workerPatterns = new Map();
|
|
1683
1701
|
/** The origins of the successful Render previews impeccable_render_previews listed, of the app's own site. */
|
|
1684
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();
|
|
1685
1705
|
let hostedPreviewProven = false;
|
|
1686
1706
|
/**
|
|
1687
1707
|
* The address pattern each proof answered, by the app it was proven for, and the last one under '' for a repository
|
|
@@ -1982,7 +2002,7 @@ export function createTools(ctx) {
|
|
|
1982
2002
|
liveApps.set(new URL(production).origin, appKey);
|
|
1983
2003
|
};
|
|
1984
2004
|
const hostCli = (host, pi, signal) => {
|
|
1985
|
-
const runner = (browser = false) =>
|
|
2005
|
+
const runner = (browser = false) => hostRunner(host, pi.cwd, { signal, browser, timeoutMs: browser ? 300_000 : 30_000 });
|
|
1986
2006
|
const cli = runner();
|
|
1987
2007
|
if (host === 'vercel') {
|
|
1988
2008
|
return {
|
|
@@ -2082,7 +2102,7 @@ export function createTools(ctx) {
|
|
|
2082
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. */
|
|
2083
2103
|
const npmInstall = (pkg, signal) => async () => {
|
|
2084
2104
|
try {
|
|
2085
|
-
await
|
|
2105
|
+
await hostRunner('npm', ownFolder(), { signal, timeoutMs: 600_000, env: installEnvironment() })(hostCliInstallArgs(pkg));
|
|
2086
2106
|
return true;
|
|
2087
2107
|
}
|
|
2088
2108
|
catch {
|
|
@@ -2090,7 +2110,7 @@ export function createTools(ctx) {
|
|
|
2090
2110
|
}
|
|
2091
2111
|
};
|
|
2092
2112
|
/** cf, Cloudflare's CLI, which only the tools run: where it is missing here, its install is asked first (#396). */
|
|
2093
|
-
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';
|
|
2094
2114
|
const installCf = (pi, signal) => installHostCli('cloudflare', () => cfPresent(pi, signal), npmInstall('cf', signal), signal);
|
|
2095
2115
|
/**
|
|
2096
2116
|
* A host's sign-in, on `impeccable_ask` with the id `<host>-setup`: the setup screen once, worded for a CLI signed in
|
|
@@ -2194,8 +2214,8 @@ export function createTools(ctx) {
|
|
|
2194
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.
|
|
2195
2215
|
if (!hostSetupAgreed(ctx.log, 'netlify') && !previewing)
|
|
2196
2216
|
return { connected: false, reason: 'The Netlify sign-in comes first: impeccable_ask with the id netlify-setup.' };
|
|
2197
|
-
const netlify =
|
|
2198
|
-
const gh =
|
|
2217
|
+
const netlify = hostRunner('netlify', pi.cwd, { signal });
|
|
2218
|
+
const gh = hostRunner('gh', pi.cwd, { signal });
|
|
2199
2219
|
const clone = ctx.hostWait?.clone;
|
|
2200
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 } : {}) });
|
|
2201
2221
|
netlifySite = result.site?.name ?? netlifySite;
|
|
@@ -2221,7 +2241,7 @@ export function createTools(ctx) {
|
|
|
2221
2241
|
async function vercelConnect(pi, signal) {
|
|
2222
2242
|
if (!hostSetupAgreed(ctx.log, 'vercel'))
|
|
2223
2243
|
return { connected: false, reason: 'The Vercel sign-in comes first: impeccable_ask with the id vercel-setup.' };
|
|
2224
|
-
const vercel =
|
|
2244
|
+
const vercel = hostRunner('vercel', pi.cwd, { signal });
|
|
2225
2245
|
const link = await readLink(pi.cwd);
|
|
2226
2246
|
if (!link)
|
|
2227
2247
|
return { connected: false, reason: 'This folder is linked to no Vercel project (.vercel/project.json): run vercel link first.' };
|
|
@@ -3179,7 +3199,7 @@ export function createTools(ctx) {
|
|
|
3179
3199
|
const vercel = (args) => {
|
|
3180
3200
|
const key = JSON.stringify(args);
|
|
3181
3201
|
if (!reads.has(key))
|
|
3182
|
-
reads.set(key,
|
|
3202
|
+
reads.set(key, hostRunner('vercel', pi.cwd, { signal })(args));
|
|
3183
3203
|
return reads.get(key);
|
|
3184
3204
|
};
|
|
3185
3205
|
for (const origin of listedPreviews)
|
|
@@ -3192,7 +3212,7 @@ export function createTools(ctx) {
|
|
|
3192
3212
|
const app = appKey !== undefined ? await vercelApp(pi, appKey, vercel) : undefined;
|
|
3193
3213
|
if (app && 'reason' in app)
|
|
3194
3214
|
return JSON.stringify({ appKey, deployments: [], unmatched: app.reason });
|
|
3195
|
-
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 }) });
|
|
3196
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.
|
|
3197
3217
|
const owned = async (d) => {
|
|
3198
3218
|
if (!app)
|
|
@@ -3273,7 +3293,7 @@ export function createTools(ctx) {
|
|
|
3273
3293
|
noteApp(appKey, { host_project: app.name });
|
|
3274
3294
|
return JSON.stringify({ ...(appKey !== undefined ? { appKey, project: app.name } : {}), ...evidence, ...(production ? { production } : {}) });
|
|
3275
3295
|
}),
|
|
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
|
|
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) => {
|
|
3277
3297
|
const pull = p.pull;
|
|
3278
3298
|
const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
|
|
3279
3299
|
if (refused)
|
|
@@ -3285,10 +3305,10 @@ export function createTools(ctx) {
|
|
|
3285
3305
|
return JSON.stringify({ appKey, sites: [], previews: [], unmatched: `The setup state names no folder for the app ${appKey}.` });
|
|
3286
3306
|
// Each site's team is for the last screen alone.
|
|
3287
3307
|
const onTeams = (teams) => Object.entries(teams).forEach(([name, team]) => accountOf.set(`netlify ${name}`, team));
|
|
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.
|
|
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.
|
|
3289
3309
|
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 } : {}) });
|
|
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 } : {}) });
|
|
3292
3312
|
// A pull request's preview is still building until Netlify's deploy of it is built or has failed.
|
|
3293
3313
|
const states = ['success', 'ready', 'failure', 'error'];
|
|
3294
3314
|
const final = (evidence) => Boolean(evidence.error) || evidence.previews.some((preview) => states.includes(preview.state ?? ''));
|
|
@@ -3325,7 +3345,7 @@ export function createTools(ctx) {
|
|
|
3325
3345
|
noteApp(appKey, { host_project: site });
|
|
3326
3346
|
return JSON.stringify(appKey !== undefined ? { appKey, ...evidence } : evidence);
|
|
3327
3347
|
}),
|
|
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
|
|
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) => {
|
|
3329
3349
|
const pull = p.pull;
|
|
3330
3350
|
const branch = p.branch;
|
|
3331
3351
|
if (pull !== undefined && branch !== undefined)
|
|
@@ -3345,10 +3365,10 @@ export function createTools(ctx) {
|
|
|
3345
3365
|
let accountName;
|
|
3346
3366
|
// A Worker Preview's build names no address: its alias is made from the pattern the plain read's Worker preview stood for.
|
|
3347
3367
|
const pattern = cloudflareEvidence?.previews.find((preview) => preview.pattern)?.pattern;
|
|
3348
|
-
// GitHub's record as the setup state holds it, where it lists Cloudflare's previews of this app; a wait on a pull request or a pushed branch reads GitHub, since its build is newer than the record.
|
|
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.
|
|
3349
3369
|
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 } : {}) });
|
|
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 } : {}) });
|
|
3352
3372
|
const read = async () => {
|
|
3353
3373
|
const evidence = await readAll();
|
|
3354
3374
|
return appProject ? { ...evidence, previews: evidence.previews.filter((preview) => pagesProject(preview.url) === appProject.label) } : evidence;
|
|
@@ -3430,8 +3450,8 @@ export function createTools(ctx) {
|
|
|
3430
3450
|
...(token ? { access: token } : {}),
|
|
3431
3451
|
});
|
|
3432
3452
|
let proved = await prove();
|
|
3433
|
-
// 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.
|
|
3434
|
-
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) {
|
|
3435
3455
|
const wait = ctx.hostWait?.serve ?? { totalMs: 90_000, everyMs: 5_000 };
|
|
3436
3456
|
said('Waiting for Render to serve the new page');
|
|
3437
3457
|
for (const end = Date.now() + wait.totalMs; proved.status === 404 && Date.now() < end && !signal?.aborted;) {
|
|
@@ -3496,7 +3516,7 @@ export function createTools(ctx) {
|
|
|
3496
3516
|
return JSON.stringify({ ...proof, appKey: app.appKey });
|
|
3497
3517
|
return JSON.stringify(proof);
|
|
3498
3518
|
}),
|
|
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
|
|
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) => {
|
|
3500
3520
|
const pull = p.pull;
|
|
3501
3521
|
const refused = appRefusal(p.appKey, 'read') ?? lockRefusal(p.appKey, 'read');
|
|
3502
3522
|
if (refused)
|
|
@@ -3511,18 +3531,19 @@ export function createTools(ctx) {
|
|
|
3511
3531
|
if (pull !== undefined && !known)
|
|
3512
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'}.`;
|
|
3513
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.
|
|
3514
|
-
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 } : {}) });
|
|
3515
3535
|
const previewsOf = (evidence, site) => evidence.deployments.flatMap((d) => {
|
|
3516
3536
|
const preview = parseRenderDeployment(d.environment, d.url);
|
|
3517
3537
|
return preview && preview.site === site && (pull === undefined || preview.pull === pull) ? [{ pull: preview.pull, url: d.url, state: d.state }] : [];
|
|
3518
3538
|
});
|
|
3519
|
-
// 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));
|
|
3520
3541
|
const list = (previews) => {
|
|
3521
3542
|
for (const origin of renderListed)
|
|
3522
3543
|
listedApps.delete(origin);
|
|
3523
3544
|
renderListed.clear();
|
|
3524
3545
|
for (const preview of previews) {
|
|
3525
|
-
if (preview.state !== 'success')
|
|
3546
|
+
if (preview.state !== 'success' || closed.has(preview.pull))
|
|
3526
3547
|
continue;
|
|
3527
3548
|
renderListed.add(new URL(preview.url).origin);
|
|
3528
3549
|
if (appKey !== undefined)
|
|
@@ -3540,15 +3561,17 @@ export function createTools(ctx) {
|
|
|
3540
3561
|
const final = (e) => Boolean(e.error) || e.previews.some((preview) => preview.state === 'success' || preview.state === 'failure' || preview.state === 'error');
|
|
3541
3562
|
const waited = await awaitBuild('Render', `Waiting for Render to build ${await subjectOf(pull)}`, read, final, () => known.page, signal);
|
|
3542
3563
|
list(waited.previews);
|
|
3564
|
+
for (const origin of renderListed)
|
|
3565
|
+
renderBuilt.add(origin);
|
|
3543
3566
|
return JSON.stringify({ ...forApp, site: known.site, ...waited });
|
|
3544
3567
|
}
|
|
3545
|
-
const render =
|
|
3568
|
+
const render = hostRunner('render', pi.cwd, { signal });
|
|
3546
3569
|
const read = await readRenderService(render, repository, folder);
|
|
3547
3570
|
const service = 'service' in read ? read.service : undefined;
|
|
3548
|
-
// GitHub's record as the setup state holds it, where it lists Render's deployments of this app; else read here.
|
|
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.
|
|
3549
3572
|
const record = hostingFor(appKey);
|
|
3550
3573
|
const recorded = record ? deploymentsOf(record).filter((d) => parseRenderDeployment(d.environment, d.url)) : [];
|
|
3551
|
-
const evidence = recorded.length ? { deployments: recorded } : await readEvidence();
|
|
3574
|
+
const evidence = recorded.length || (record && hostingRead(ctx.hosting, 'render')) ? { deployments: recorded } : await readEvidence();
|
|
3552
3575
|
let site = service?.site;
|
|
3553
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.
|
|
3554
3577
|
if (!site && record) {
|
|
@@ -3574,7 +3597,7 @@ export function createTools(ctx) {
|
|
|
3574
3597
|
const unread = 'reason' in read ? { unread: read.reason } : {};
|
|
3575
3598
|
if (!site)
|
|
3576
3599
|
return JSON.stringify({ ...forApp, previews: [], cli, ...unread, ...(evidence.error ? { error: evidence.error } : {}) });
|
|
3577
|
-
const previews = previewsOf(evidence, site);
|
|
3600
|
+
const previews = previewsOf(evidence, site).map((p) => (closed.has(p.pull) ? { ...p, pullState: 'closed' } : p));
|
|
3578
3601
|
list(previews);
|
|
3579
3602
|
const production = service?.production ?? `https://${site}.onrender.com`;
|
|
3580
3603
|
noteProduction(production, 'render', undefined, appKey);
|
|
@@ -3652,8 +3675,8 @@ export function createTools(ctx) {
|
|
|
3652
3675
|
// cf is installed here: the fact this acts on is one a read through cf found.
|
|
3653
3676
|
const { status, ...more } = await turnOnPreviews(host, async () => {
|
|
3654
3677
|
if (host === 'netlify')
|
|
3655
|
-
return enableNetlifyPreviews(
|
|
3656
|
-
const cf = cfRunner({ checkout: pi.cwd },
|
|
3678
|
+
return enableNetlifyPreviews(hostRunner('netlify', pi.cwd, { signal }), fact.project);
|
|
3679
|
+
const cf = cfRunner({ checkout: pi.cwd }, hostRunner);
|
|
3657
3680
|
return enableCloudflarePreviews(cf, fact.project, await cfStatus(cf, signal), signal);
|
|
3658
3681
|
}, signal);
|
|
3659
3682
|
return answer(status, more);
|
|
@@ -3687,8 +3710,8 @@ export function createTools(ctx) {
|
|
|
3687
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)
|
|
3688
3711
|
return answer('skipped', { reason: `The person skipped ${create ? 'creating the Pages project' : 'connecting the repository to the Pages project'}.` });
|
|
3689
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.
|
|
3690
|
-
const cf = cfRunner({ checkout: pi.cwd },
|
|
3691
|
-
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);
|
|
3692
3715
|
const writes = (status) => status.cli === 'signed-in' && (!status.scopes || status.scopes.includes('pages:write'));
|
|
3693
3716
|
let status = await cfStatus(cf, signal);
|
|
3694
3717
|
// cf not on this machine (#396): its install is asked first.
|
|
@@ -3718,7 +3741,7 @@ export function createTools(ctx) {
|
|
|
3718
3741
|
}
|
|
3719
3742
|
}
|
|
3720
3743
|
said(create ? `Creating the Pages project ${project}` : `Connecting the repository to ${project}`);
|
|
3721
|
-
const gh =
|
|
3744
|
+
const gh = hostRunner('gh', pi.cwd, { signal });
|
|
3722
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);
|
|
3723
3746
|
if (outcome.status !== 'connected')
|
|
3724
3747
|
return answer('refused', { reason: outcome.reason });
|
|
@@ -3757,7 +3780,7 @@ export function createTools(ctx) {
|
|
|
3757
3780
|
said("Getting past Vercel's login");
|
|
3758
3781
|
if (at?.app)
|
|
3759
3782
|
lockPending.delete(at.app.appKey);
|
|
3760
|
-
const vercel =
|
|
3783
|
+
const vercel = hostRunner('vercel', pi.cwd, { signal });
|
|
3761
3784
|
// Before the push, the app's project as impeccable_hosted_previews reads it: the one that builds the app's folder (#281).
|
|
3762
3785
|
let app = at?.app;
|
|
3763
3786
|
if (before && appsRepo) {
|
|
@@ -3992,7 +4015,7 @@ export function createTools(ctx) {
|
|
|
3992
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. */
|
|
3993
4016
|
let replaced = false;
|
|
3994
4017
|
let answer;
|
|
3995
|
-
const netlify =
|
|
4018
|
+
const netlify = hostRunner('netlify', pi.cwd, { signal });
|
|
3996
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.
|
|
3997
4020
|
const run = async (args) => {
|
|
3998
4021
|
try {
|
|
@@ -4157,8 +4180,8 @@ export function createTools(ctx) {
|
|
|
4157
4180
|
accessConsented.add(found.id);
|
|
4158
4181
|
return yes;
|
|
4159
4182
|
},
|
|
4160
|
-
cf: cfRunner({ checkout: pi.cwd },
|
|
4161
|
-
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),
|
|
4162
4185
|
retry: async () => !(await ask(HOST_SCREENS['cloudflare-signin-retry'])).skipped,
|
|
4163
4186
|
install: async () => void (await installCf(pi, signal)),
|
|
4164
4187
|
status: said,
|