@impeccable/reviews 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin.js +0 -0
- package/dist/fence.js +4 -0
- package/dist/host-questions.js +47 -1
- package/dist/netlify-password.js +62 -7
- package/dist/playbook/cases/netlify-previews.md +2 -2
- package/dist/playbook/cases/netlify-setup.md +2 -2
- package/dist/playbook/cases/preview-protection.md +7 -5
- package/dist/playbook/cases/pull-requests.md +2 -2
- package/dist/playbook/cases/server-previews.md +2 -2
- package/dist/playbook/cases/vercel-setup.md +5 -5
- package/dist/playbook/github-previews.js +101 -0
- package/dist/playbook/netlify-pattern.js +24 -0
- package/dist/playbook/netlify-previews.js +144 -0
- package/dist/playbook/probe-preview.js +67 -0
- package/dist/playbook/protection-secrets.js +15 -0
- package/dist/playbook/public-preview-request.js +96 -0
- package/dist/playbook/publication-wait.js +46 -0
- package/dist/playbook/vercel-protection.js +121 -0
- package/dist/playbook.js +2 -1
- package/dist/tools.js +126 -50
- package/dist/vercel.js +27 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,6 @@ A test account's password, typed hidden on the account's screen (after a screen
|
|
|
84
84
|
|
|
85
85
|
Vercel's bypass secret, which lets reviews open previews behind Vercel's login, is handled the same way. When Vercel's login stands in front of a preview, the command asks you once, on one screen, whether to create a bypass secret for Impeccable, and on your yes creates it as you through the Vercel CLI, labelled "Impeccable reviews" (`vercel api` for Vercel's protection-bypass route; an older CLI's `vercel project protection enable --protection-bypass`), then reads it back from the project's settings. A project that already holds a secret with that label gives it without a question. When the CLI is signed out, this folder is not linked, or your role cannot make one, a screen names the settings page and takes the secret you type, hidden. It opens the preview with the secret first and saves it, with the setting that sends it in the `x-vercel-protection-bypass` header, only once the page shows your app. The agent never holds it: the secret goes from the command's own process to Vercel and to Impeccable's setting, and the debug log and the transcript remove it. Vercel documents this as Protection Bypass for Automation, which every plan includes; you revoke it in the project's Settings, Deployment Protection.
|
|
86
86
|
|
|
87
|
-
Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password" on a credit-based Pro plan) is handled the same way. When a Netlify deploy preview asks for it, the command reads the site's settings through the Netlify CLI, signed in as you. For a password Netlify gives back, it asks you once, on one screen, whether to store the site password for Impeccable, and on your yes reads it from the site as you. When Netlify does not give the password back, the CLI is signed out or the site is in none of your teams, a screen takes the password you type, hidden (the run never sets one itself: the Netlify CLI takes a new password only on its command line, where any process could read it). It opens the preview with the password first, as basic credentials in the `Authorization` header (Netlify's password page asks for a password alone, so the credentials carry no username), and saves it, with the setting that sends that header, only once the page shows your app; a password that opens the preview on Netlify's own form but not in the header is reported, not saved, since reviews can send only a header. A site behind Netlify's team login (a private site
|
|
87
|
+
Netlify's site password (Password Protection on Pro and Enterprise plans; "Project visibility: Password" on a credit-based Pro plan) is handled the same way. When a Netlify deploy preview asks for it, the command reads the site's settings through the Netlify CLI, signed in as you. For a password Netlify gives back, it asks you once, on one screen, whether to store the site password for Impeccable, and on your yes reads it from the site as you. When Netlify does not give the password back, the CLI is signed out or the site is in none of your teams, a screen takes the password you type, hidden (the run never sets one itself: the Netlify CLI takes a new password only on its command line, where any process could read it). It opens the preview with the password first, as basic credentials in the `Authorization` header (Netlify's password page asks for a password alone, so the credentials carry no username), and saves it, with the setting that sends that header, only once the page shows your app; a password that opens the preview on Netlify's own form but not in the header is reported, not saved, since reviews can send only a header. A site behind Netlify's team login (a private site) has no way in for tools, so the command asks you once, on one screen, whether to make the site public, and on your yes makes it public as you through the Netlify CLI (`netlify api updateSite` with `sso_login` off); anyone with its address can then open it, the live site included, until you make it private again in Project configuration, General, Visitor access. Where Netlify refuses (your team's default keeps every project private, or your role cannot change it), the command opens that page in your browser and asks you to try again once you changed it there; a password you set there instead is stored as above. The agent never holds the password: it goes from the command's own process to Netlify and to Impeccable's setting, and the debug log and the transcript remove it.
|
|
88
88
|
|
|
89
89
|
A Cloudflare Access service token, which lets reviews open Pages previews behind Access, is handled the same way. When Access stands in front of a preview, the command asks you once, on one screen, whether to create a service token for Impeccable. On your yes it creates the token as you through Cloudflare's CLI, cf (run through npx), named "Impeccable reviews", and allows it in a Service Auth policy of the Access application in front of the preview (a reusable policy added to the application's list). When cf here is not signed in with Access, your browser opens Cloudflare's sign-in first, for five scopes (`account:read`, `user:read`, `pages:read`, `access.write`, `access-service-token.write`), with a retry when it did not complete. Only where that sign-in cannot create the token (it grants no Access, cf does not run here, or the application runs on policies of its own, which cf has no command for) does the command ask for a Cloudflare API token with two permissions (Access: Apps and Policies, Edit; Access: Service Tokens, Edit), used for that run only and stored nowhere. On a skip, or where that cannot be done, a screen takes a client id and secret you paste, the secret hidden. It opens the preview with the token in the `CF-Access-Client-Id` and `CF-Access-Client-Secret` headers first and saves it, with the setting that sends them, only once the page shows your app; a token it made that opens nothing is deleted, with its policy. The agent never holds either, and the debug log and the transcript remove both. You revoke it in Zero Trust, Access controls, Service credentials, Service Tokens.
|
package/dist/bin.js
CHANGED
|
File without changes
|
package/dist/fence.js
CHANGED
|
@@ -1183,6 +1183,10 @@ export function chooseHostTeam(log, questionId, team) {
|
|
|
1183
1183
|
if (host && Object.hasOwn(HOST_CLIS, host) && hostsAgreed(log).has(host) && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(team))
|
|
1184
1184
|
teamsChosen(log).set(host, team);
|
|
1185
1185
|
}
|
|
1186
|
+
/** The team picked or found for a host's CLI in this run, which its dashboard's addresses name. */
|
|
1187
|
+
export function chosenHostTeam(log, host) {
|
|
1188
|
+
return teamsChosen(log).get(host);
|
|
1189
|
+
}
|
|
1186
1190
|
/** What impeccable_vercel_connect settled of the linked Vercel project's first deployment in this run, by the run's log. */
|
|
1187
1191
|
const settledVercel = new WeakMap();
|
|
1188
1192
|
export function settleVercelFirstDeployment(log, settlement) {
|
package/dist/host-questions.js
CHANGED
|
@@ -21,7 +21,20 @@ export const HOST_QUESTIONS = {
|
|
|
21
21
|
};
|
|
22
22
|
/** Where a person reads or changes a site's password in Netlify, as Netlify's docs name the page on every plan. */
|
|
23
23
|
export const NETLIFY_PASSWORD_WHERE = 'Project configuration → General → Visitor access';
|
|
24
|
-
/** The
|
|
24
|
+
/** The wait for a host's build that ran out: the page where the host's builds of pull requests are turned on, and the wait again. */
|
|
25
|
+
const buildWait = (id, name, where) => ({
|
|
26
|
+
[`${id}-build`]: {
|
|
27
|
+
id: `${id}-build`,
|
|
28
|
+
kind: 'confirm',
|
|
29
|
+
title: `Waiting for ${name}`,
|
|
30
|
+
hint: `Five minutes and no preview yet. If ${name} hasn't started a build, ${where} on the page that opens. The run carries on by itself once the preview is built.`,
|
|
31
|
+
prompt: `Open ${name} and keep waiting?`,
|
|
32
|
+
confirmLabel: 'Keep waiting',
|
|
33
|
+
skipLabel: 'Skip',
|
|
34
|
+
},
|
|
35
|
+
[`${id}-build-retry`]: { id: `${id}-build-retry`, kind: 'confirm', title: `Waiting for ${name}`, prompt: `Still no preview from ${name}.`, confirmLabel: 'Keep waiting', skipLabel: 'Skip' },
|
|
36
|
+
});
|
|
37
|
+
/** The screens a tool shows on its own: a sign-in's retry, the one-time access the host's GitHub app needs, and the consent to a key for a host's lock (a bypass secret for Vercel's login, the site password for Netlify's, a service token for Cloudflare Access), with the paste where the run does not get one, and to making a site behind Netlify's team login public. */
|
|
25
38
|
export const HOST_SCREENS = {
|
|
26
39
|
'netlify-signin-retry': { id: 'netlify-signin-retry', kind: 'confirm', title: 'Sign in to Netlify', prompt: 'The sign-in did not complete.', confirmLabel: 'Try again', skipLabel: 'Skip' },
|
|
27
40
|
'netlify-access': {
|
|
@@ -45,6 +58,20 @@ export const HOST_SCREENS = {
|
|
|
45
58
|
skipLabel: 'Skip',
|
|
46
59
|
},
|
|
47
60
|
'vercel-access-retry': { id: 'vercel-access-retry', kind: 'confirm', title: 'Vercel needs access on GitHub', prompt: "Vercel still can't see the repository.", confirmLabel: 'Try again', skipLabel: 'Skip' },
|
|
61
|
+
// Vercel refused the run's lock on a new project: the person turns it on, since the test drive's push is the project's first deployment.
|
|
62
|
+
'vercel-lock': {
|
|
63
|
+
id: 'vercel-lock',
|
|
64
|
+
kind: 'confirm',
|
|
65
|
+
title: 'Lock the new Vercel project',
|
|
66
|
+
hint: "The test drive's push is the project's first deployment, which Vercel shows to anyone at the project's own address. Vercel didn't let the run put its login in front. Turn on Vercel Authentication for All Deployments on the page that opens. The run carries on by itself once it's on.",
|
|
67
|
+
prompt: 'Open Vercel to lock the project?',
|
|
68
|
+
confirmLabel: 'Open Vercel',
|
|
69
|
+
skipLabel: 'Skip',
|
|
70
|
+
},
|
|
71
|
+
'vercel-lock-retry': { id: 'vercel-lock-retry', kind: 'confirm', title: 'Lock the new Vercel project', prompt: "The project isn't locked yet.", confirmLabel: 'Try again', skipLabel: 'Skip' },
|
|
72
|
+
...buildWait('vercel', 'Vercel', 'connect the repository in Settings → Git'),
|
|
73
|
+
...buildWait('netlify', 'Netlify', 'turn Deploy Previews on'),
|
|
74
|
+
...buildWait('cloudflare', 'Cloudflare', 'connect the repository to the Pages project'),
|
|
48
75
|
// Vercel's login in front of the previews: the one consent to a bypass secret the run creates as the person, and the paste when the CLI cannot.
|
|
49
76
|
'vercel-bypass': {
|
|
50
77
|
id: 'vercel-bypass',
|
|
@@ -81,6 +108,25 @@ export const HOST_SCREENS = {
|
|
|
81
108
|
prompt: `Netlify keeps your previews behind the site password, and the run could not read it: the Netlify CLI here is signed out, the site is in none of your teams, or Netlify does not give the password back. Find it, or set a new one, in ${NETLIFY_PASSWORD_WHERE}. It is stored encrypted and used only to open previews.`,
|
|
82
109
|
hint: 'Paste the password',
|
|
83
110
|
},
|
|
111
|
+
// Netlify's team login in front of the previews: the one consent to making the site public as the person, and the retry once they changed it on Netlify's page where Netlify refused.
|
|
112
|
+
'netlify-visibility': {
|
|
113
|
+
id: 'netlify-visibility',
|
|
114
|
+
kind: 'confirm',
|
|
115
|
+
title: 'Your Netlify previews are private',
|
|
116
|
+
hint: `Impeccable reviews each pull request on its preview, and Netlify keeps this site behind your team's login, which tools can't get past. The run makes the site public as you, so anyone with an address of it can open it, the live site too. Change it back any time in ${NETLIFY_PASSWORD_WHERE}.`,
|
|
117
|
+
prompt: 'Make the site public?',
|
|
118
|
+
confirmLabel: 'Make public',
|
|
119
|
+
skipLabel: 'Skip',
|
|
120
|
+
},
|
|
121
|
+
'netlify-visibility-retry': {
|
|
122
|
+
id: 'netlify-visibility-retry',
|
|
123
|
+
kind: 'confirm',
|
|
124
|
+
title: 'Your Netlify previews are private',
|
|
125
|
+
hint: "Netlify still shows its login in front of the previews. On the Netlify page that opened, set Visitor access to Public (or Password protected, on Pro). Public is greyed out until the site has a live deploy, or while your team's default keeps it private (Team settings → General → Visitor access).",
|
|
126
|
+
prompt: 'Changed it?',
|
|
127
|
+
confirmLabel: 'Try again',
|
|
128
|
+
skipLabel: 'Skip',
|
|
129
|
+
},
|
|
84
130
|
// Cloudflare Access in front of the previews: the one consent to a service token the run creates as the person through cf, the retry of cf's sign-in, the API token where that sign-in cannot create it, and the paste where the run does not create one.
|
|
85
131
|
'cloudflare-access': {
|
|
86
132
|
id: 'cloudflare-access',
|
package/dist/netlify-password.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* no password goes back to the model. The person consents once, on the tool's screen, before the password is stored;
|
|
8
8
|
* a password opens the locked address before anything saves it. A password Netlify does not give back is the
|
|
9
9
|
* person's to type: setting one through the CLI would put it on the command line, where any process could read it.
|
|
10
|
-
* Netlify's team login (a private site
|
|
10
|
+
* Netlify's team login (a private site) has no way in for tools, so on the person's yes the run makes the site public
|
|
11
|
+
* through the CLI; where Netlify refuses, the person changes Visitor access on Netlify's page and the run tries again.
|
|
11
12
|
*/
|
|
12
13
|
/** The header reviews send the password in. */
|
|
13
14
|
export const PASSWORD_HEADER = 'Authorization';
|
|
@@ -15,6 +16,13 @@ export const PASSWORD_HEADER = 'Authorization';
|
|
|
15
16
|
export const basicCredentials = (password) => `Basic ${Buffer.from(`:${password}`).toString('base64')}`;
|
|
16
17
|
/** The CLI's read of a site by the address every deploy preview of it carries (`site_id` takes the site's default domain), an authenticated call of Netlify's API. */
|
|
17
18
|
export const readSiteCall = (site) => ['api', 'getSite', '--data', JSON.stringify({ site_id: `${site}.netlify.app` })];
|
|
19
|
+
/**
|
|
20
|
+
* The CLI's change of a site's visitor access to Public: the team login off. `updateSite` documents no such field, but
|
|
21
|
+
* `getSite` reads it back and Netlify's own tools send it; `sso_login_context` stays out, since Netlify answers 422 with it.
|
|
22
|
+
*/
|
|
23
|
+
export const publicSiteCall = (id) => ['api', 'updateSite', '--data', JSON.stringify({ site_id: id, body: { sso_login: false } })];
|
|
24
|
+
/** Netlify's page where a site's Visitor access is changed. */
|
|
25
|
+
export const visitorAccessPage = (site) => `https://app.netlify.com/projects/${site}/configuration/general#visitor-access`;
|
|
18
26
|
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
|
|
19
27
|
/** A site's name and id as Netlify makes them. */
|
|
20
28
|
const NAME = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -38,11 +46,18 @@ export async function readSite(run, site) {
|
|
|
38
46
|
/**
|
|
39
47
|
* Gets the site password in place for the address Netlify's lock stands in front of, and saves only one that opens it.
|
|
40
48
|
* The site's settings say what the lock is: a password, which the person's yes has the run store when Netlify gives it
|
|
41
|
-
* back; the team login, which
|
|
42
|
-
* password back, the person may paste the password.
|
|
49
|
+
* back; the team login, which the person's yes has the run lift by making the site public; or neither. When the CLI
|
|
50
|
+
* cannot read the site, or Netlify gives no password back, the person may paste the password.
|
|
43
51
|
*/
|
|
44
52
|
export async function passNetlifyLock(deps) {
|
|
45
|
-
|
|
53
|
+
let site = await readSite(deps.run, deps.site);
|
|
54
|
+
if (site?.teamLogin && !site.hasPassword) {
|
|
55
|
+
const opened = await openToVisitors(deps, site);
|
|
56
|
+
if ('status' in opened)
|
|
57
|
+
return opened;
|
|
58
|
+
// The person picked Password protected on Netlify's page: the password is got in place as any site password is.
|
|
59
|
+
site = opened;
|
|
60
|
+
}
|
|
46
61
|
const from = site ? { site: site.name } : {};
|
|
47
62
|
const finish = async (password, how) => {
|
|
48
63
|
deps.remember(password);
|
|
@@ -73,9 +88,6 @@ export async function passNetlifyLock(deps) {
|
|
|
73
88
|
return { status: 'refused', reason: done.locked ? "Netlify's own site password opened the address neither in the header nor on Netlify's form: the page asks for the app's own sign-in past the lock, or Netlify did not answer. Prove a page a visitor opens without signing in, and call this tool again." : done.reason, protection: 'password', ...from };
|
|
74
89
|
}
|
|
75
90
|
}
|
|
76
|
-
else if (site.teamLogin) {
|
|
77
|
-
return { status: 'login', protection: 'team-login', reason: "Netlify's team login stands in front of the previews: they open for a member of the Netlify team signed in in their browser, and Netlify gives tools no way in.", ...from };
|
|
78
|
-
}
|
|
79
91
|
else {
|
|
80
92
|
return { status: 'refused', protection: 'none', reason: "Netlify's settings show neither a password nor the team login on the site: the sign-in is the app's own, so prove a page a visitor opens without signing in.", ...from };
|
|
81
93
|
}
|
|
@@ -87,3 +99,46 @@ export async function passNetlifyLock(deps) {
|
|
|
87
99
|
const done = await finish(typed, 'typed');
|
|
88
100
|
return 'status' in done ? done : { status: 'refused', reason: done.locked ? `The password typed did not open the address: it is not the site's password, or the page asks for the app's own sign-in past the lock. ${done.reason}` : done.reason, ...from };
|
|
89
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* A site behind Netlify's team login made public on the person's yes: the CLI's change, read back, then the preview
|
|
104
|
+
* proven. Where Netlify refuses (the team's default keeps every project private, the field is not taken, the role
|
|
105
|
+
* cannot) or the preview still asks for the login, its Visitor access page opens and the person tries again, twice at
|
|
106
|
+
* most. A password the person set there instead comes back as the site, for the password's own steps.
|
|
107
|
+
*/
|
|
108
|
+
async function openToVisitors(deps, site) {
|
|
109
|
+
const from = { site: site.name, protection: 'team-login' };
|
|
110
|
+
if (!(await deps.makePublic()))
|
|
111
|
+
return { status: 'skipped', reason: 'The person skipped making the site public.', ...from };
|
|
112
|
+
let read = site;
|
|
113
|
+
/** The run's change went through: the site stays public whatever comes after, and every later answer says so. */
|
|
114
|
+
let ranPublic = false;
|
|
115
|
+
const kept = () => (ranPublic ? { madePublic: 'run' } : {});
|
|
116
|
+
for (let tries = 0;; tries++) {
|
|
117
|
+
if (read?.teamLogin && !read.hasPassword) {
|
|
118
|
+
try {
|
|
119
|
+
await deps.run(publicSiteCall(read.id));
|
|
120
|
+
read = await readSite(deps.run, deps.site);
|
|
121
|
+
if (read && !read.teamLogin)
|
|
122
|
+
ranPublic = true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Refused: the site as read stays, and the person's page is next.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (read?.hasPassword)
|
|
129
|
+
return read;
|
|
130
|
+
if (read && !read.teamLogin) {
|
|
131
|
+
const proof = await deps.prove(undefined, false);
|
|
132
|
+
if (proof.proven)
|
|
133
|
+
return { status: 'opened', madePublic: ranPublic ? 'run' : 'person', ...from };
|
|
134
|
+
// Past the login, a page that is not the app's: the proof says why.
|
|
135
|
+
if (proof.gate === undefined)
|
|
136
|
+
return { status: 'refused', reason: proof.reason ?? 'The address did not open.', site: site.name, protection: 'none', ...kept() };
|
|
137
|
+
}
|
|
138
|
+
if (tries === 2)
|
|
139
|
+
return { status: 'login', reason: "Netlify kept its team login in front of the previews.", ...from, ...kept() };
|
|
140
|
+
if (!(await deps.retry(visitorAccessPage(site.name), tries === 0)))
|
|
141
|
+
return { status: 'skipped', reason: "The person skipped changing Visitor access on Netlify's page.", ...from, ...kept() };
|
|
142
|
+
read = await readSite(deps.run, deps.site);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -9,9 +9,9 @@ Netlify deploys previews of the pull requests: impeccable_netlify_previews lists
|
|
|
9
9
|
|
|
10
10
|
### Reference
|
|
11
11
|
|
|
12
|
-
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the site password, or Netlify's team login, stands in front of the previews. That is the preview-protection case's, whose steps come before step 2: the password is
|
|
12
|
+
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the site password, or Netlify's team login, stands in front of the previews. That is the preview-protection case's, whose steps come before step 2: on the person's yes the password is stored, or the site behind the team login made public.
|
|
13
13
|
- **Another app, or no answer from any of the three**: Netlify's previews do not show this app. Set the preview up as server-previews or static-site says instead, and say under "What is not verified" what the Netlify address answered.
|
|
14
14
|
- **Nothing listed**: Netlify's previews are not on record. Where the checkout is set up for Netlify that is the netlify-setup case's; else set the preview up as server-previews or static-site says. An `error` means a read failed, so a preview may exist that is not listed: say so under "What is not verified".
|
|
15
15
|
- **The pull requests**: no setup pull request; the test drive branches from the default branch. Netlify builds a deploy preview only for a pull request, so the test drive opens as a draft, its deploy preview is waited for and the test drive's page proven on it, and then the draft is marked ready: pull-requests' step 4. In place of pull-requests' step 3, build the test drive branch once with the build Netlify runs (`netlify.toml`'s `[build]` `command`, else the package's `build` script), so a page that does not build is never pushed.
|
|
16
|
-
- The evidence is read only: link, deploy and change nothing on Netlify. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
|
|
16
|
+
- The evidence is read only: link, deploy and change nothing on Netlify, beyond what impeccable_netlify_password does on the person's yes. Netlify does not report its previews as GitHub deployments, so Impeccable finds them by their address pattern: save the pattern the proof answered, and no other.
|
|
17
17
|
- The report names the address that answered with this app under "What we found", and the pattern saved under "What was configured, and by whom". The test drive's own preview, which Netlify builds after the run, goes under "What is not verified".
|
|
@@ -18,7 +18,7 @@ The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a
|
|
|
18
18
|
- **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the site not created, the repository not connected): the Netlify setup ends there. Set the preview up as server-previews or static-site says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Netlify's GitHub app access to the repository", with the `access` page in the report.
|
|
19
19
|
- **No preview for the draft**: save no method, set up no other preview, and leave the draft as it is, unrecorded, as pull-requests' "No preview, no test drive" says; the site and the connection stay, and the report names them.
|
|
20
20
|
- **The build failed** (`state` is `error` or `failure`): say so under "What is not verified", with the command that builds this linked checkout as Netlify does and prints why, for the person to run: `netlify deploy --build --context deploy-preview`. It never runs here, since it builds with their Netlify sign-in.
|
|
21
|
-
- **No deploy preview within five minutes**: the
|
|
22
|
-
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the preview-protection case's, whose steps come before the save in step 5. A team that keeps new sites private puts Netlify's team login in front of the new site's previews
|
|
21
|
+
- **No deploy preview within five minutes**: the wait's own screen opens the site's deploy settings, where Deploy Previews are turned on, and waits again on the person's yes. When they skip it, or none came after its retries, say under "What is not verified" that Netlify built no deploy preview here, and that Deploy Previews may be off in the site's settings.
|
|
22
|
+
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): the preview-protection case's, whose steps come before the save in step 5. A team that keeps new sites private puts Netlify's team login in front of the new site's previews: the tool makes the site public on the person's yes, and step 5 goes on once it answers `opened`.
|
|
23
23
|
- **The report** names the Netlify site under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Netlify account, with how the repository was connected and the CLI's install when there was one. The site's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
|
|
24
24
|
- **Status lines**: the sign-in, the link and the connection belong to the Preview method task; from the page on, the Test drive task, so name the test drive in each line about its build, its push, the draft, the wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Netlify's login as a login, since a line about signing in belongs to the Sign-in task.
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
### When this applies
|
|
2
2
|
|
|
3
|
-
impeccable_prove_preview answered a `gate`: the host's own lock stands in front of a preview the host made, and a visitor gets its login or its password page. Three locks have a way in for a tool, each with a tool of its own that works as the person in the command's own process, so no key ever reaches you: Vercel's Deployment Protection (`gate: "vercel-protection"`: Vercel Authentication, or Password Protection, which shows the same gate), whose way in is a bypass secret of the project, sent in a header, which every plan has; Netlify's site password (`gate: "netlify-protection"`), sent as basic credentials in the Authorization header; and Cloudflare Access in front of a Pages preview (`gate: "cloudflare-access"`), whose way in is a service token that a Service Auth policy of the Access application allows, sent in two headers. Netlify's team login, which the same gate answers for, has none. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well, and so does netlify-setup's new site on a team that keeps new sites private: these steps come between the proof and the save of the method, in vercel-previews', netlify-previews' or cloudflare-previews' step 2 or vercel-setup's or netlify-setup's step 5, before the test drive's pull request opens (on Netlify, before its draft is marked ready), and end with the preview opened.
|
|
3
|
+
impeccable_prove_preview answered a `gate`: the host's own lock stands in front of a preview the host made, and a visitor gets its login or its password page. Three locks have a way in for a tool, each with a tool of its own that works as the person in the command's own process, so no key ever reaches you: Vercel's Deployment Protection (`gate: "vercel-protection"`: Vercel Authentication, or Password Protection, which shows the same gate), whose way in is a bypass secret of the project, sent in a header, which every plan has; Netlify's site password (`gate: "netlify-protection"`), sent as basic credentials in the Authorization header; and Cloudflare Access in front of a Pages preview (`gate: "cloudflare-access"`), whose way in is a service token that a Service Auth policy of the Access application allows, sent in two headers. Netlify's team login, which the same gate answers for, has none, so the tool makes the site public on the person's yes. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well, and so does netlify-setup's new site on a team that keeps new sites private: these steps come between the proof and the save of the method, in vercel-previews', netlify-previews' or cloudflare-previews' step 2 or vercel-setup's or netlify-setup's step 5, before the test drive's pull request opens (on Netlify, before its draft is marked ready), and end with the preview opened.
|
|
4
4
|
|
|
5
5
|
### Steps
|
|
6
6
|
|
|
7
|
-
1. **Get the key in place** (the Preview method task): impeccable_vercel_bypass for Vercel's login, under the status line *"Getting past Vercel's login"*; impeccable_netlify_password for Netlify's lock, under *"Getting past Netlify's password"*; impeccable_cloudflare_access for Cloudflare Access, under *"Getting past Cloudflare Access"*. The tool does the rest: it finds the project's secret labelled "Impeccable reviews", or reads what Netlify's settings say stands in front of the site; shows one screen asking to create the secret, to store the site password Netlify gives back, or to create a service token (then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one; opens the preview with it; and saves it for reviews. The key never comes back to you. Done when it answered `status`:
|
|
7
|
+
1. **Get the key in place** (the Preview method task): impeccable_vercel_bypass for Vercel's login, under the status line *"Getting past Vercel's login"*; impeccable_netlify_password for Netlify's lock, under *"Getting past Netlify's password"*; impeccable_cloudflare_access for Cloudflare Access, under *"Getting past Cloudflare Access"*. The tool does the rest: it finds the project's secret labelled "Impeccable reviews", or reads what Netlify's settings say stands in front of the site; shows one screen asking to create the secret, to store the site password Netlify gives back, to make a site behind Netlify's team login public (where Netlify refuses, it opens Netlify's Visitor access page and asks the person to try again once they changed it there), or to create a service token (then Cloudflare's sign-in in the browser where cf here lacks it, and it creates the token as the person and allows it in the Access application's policy; a screen takes a Cloudflare API token only where that sign-in cannot), or, when it cannot make or read one, one screen taking a pasted one (Vercel's with the page where it is made opened); opens the preview with it; and saves it for reviews. The key never comes back to you. Done when it answered `status`:
|
|
8
8
|
- `saved`: the preview is proven (`proven: true`); `secret`, `password` or `token` says whether it was created, existing or typed, and `project`, `site` or `application` which one holds or allows it; Cloudflare's `pattern` is the address pattern to save with the method, and `apiToken` says a Cloudflare API token the person made created the token. Step 2.
|
|
9
|
-
- `
|
|
9
|
+
- `opened` (Netlify): the site is public now and the preview proven (`proven: true`); `madePublic` says whether the run or the person made it public, and `site` which site. Step 2.
|
|
10
|
+
- `login` (Netlify): Netlify kept its team login in front through the retries; `reason` says so. Step 3.
|
|
11
|
+
- Any answer with `madePublic` but `opened`: the run made the Netlify site public and it stays so, though the preview did not open; the report names that change as the bullet below says, whatever step comes next.
|
|
10
12
|
- `refused` whose `reason` says to prove a page a visitor opens: Netlify's own site password opened neither the header nor Netlify's form, so the page you proved asks for the app's own sign-in past the lock, or Netlify did not answer. Prove a page a visitor opens without signing in, as netlify-previews' step 1 says, and call the tool again; the consent is not asked a second time. Refused again, step 3.
|
|
11
13
|
- Any other `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
|
|
12
14
|
- `save-failed`: the setup API did not take it all; `reason` says how far it got. Call the tool once more; when it fails again, step 3, with "Save the Vercel bypass secret on the setup page", "Save the Netlify site password on the setup page" or "Save the Cloudflare service token on the setup page" in `left`.
|
|
13
15
|
2. **Check it from Impeccable's side**: impeccable_write to `connection-check` with `{"appKey": "main"}`, and report what it answered. Then go on with the case that sent you here: the proof stands, so save the method as vercel-previews', netlify-previews' or cloudflare-previews' step 2 says, Cloudflare's with the tool's `pattern`. Done when the method is saved.
|
|
14
|
-
3. **When nothing was saved**, save no method and set up no other preview, whatever the checkup's `next` says, and open no test drive (pull-requests' "No preview, no test drive"). Say under "What is not verified" that the previews are behind the host's lock, with the tool's `reason`, and name the settings page where the person makes the key (the report's bullet below); end the report's "Not part of this setup" line with the lock the tool met: "Vercel's Deployment Protection in front of the previews", "Netlify's password in front of the previews", "Netlify's team login in front of the previews", or "Cloudflare Access in front of the previews". Done when the report says so.
|
|
16
|
+
3. **When nothing was saved**, save no method and set up no other preview, whatever the checkup's `next` says, and open no test drive (pull-requests' "No preview, no test drive"). Say under "What is not verified" that the previews are behind the host's lock, with the tool's `reason`, and name the settings page where the person makes the key, or makes the Netlify site public (the report's bullet below); put the person's one step in `left`, the settings action alone and never running the setup again (for Netlify's team login, "Make the Netlify site public in Visitor access", unless the answer has `madePublic`: the site is public already, so the step comes from the tool's `reason`); end the report's "Not part of this setup" line with the lock the tool met: "Vercel's Deployment Protection in front of the previews", "Netlify's password in front of the previews", "Netlify's team login in front of the previews", or "Cloudflare Access in front of the previews". Done when the report says so.
|
|
15
17
|
|
|
16
18
|
### Reference
|
|
17
19
|
|
|
18
|
-
- **The report** names, under "What was configured, and by whom", the Vercel project that holds the secret named "Impeccable reviews" (`project`), whether this run created it, found it there or the person typed it, and where it is revoked: the project's Settings → Deployment Protection → Protection Bypass for Automation; or the Netlify site (`site`) whose password is stored, whether this run read it from the site or the person typed it, and where it is changed: the site's Project configuration → General → Visitor access; or the service token named "Impeccable reviews", whether this run created it or the person typed it, the Access application whose policy allows it (`application`), when a created one runs out (`expires`), and where it is revoked: Zero Trust → Access controls → Service credentials → Service Tokens; with `apiToken`, the Cloudflare API token the person made for the run is theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until
|
|
20
|
+
- **The report** names, under "What was configured, and by whom", the Vercel project that holds the secret named "Impeccable reviews" (`project`), whether this run created it, found it there or the person typed it, and where it is revoked: the project's Settings → Deployment Protection → Protection Bypass for Automation; or the Netlify site (`site`) whose password is stored, whether this run read it from the site or the person typed it, and where it is changed: the site's Project configuration → General → Visitor access; or, with `opened`, the Netlify site made public, by this run on the person's yes or by the person on Netlify's page (`madePublic`), so anyone with its address opens it, and where it is made private again: the same Visitor access; or the service token named "Impeccable reviews", whether this run created it or the person typed it, the Access application whose policy allows it (`application`), when a created one runs out (`expires`), and where it is revoked: Zero Trust → Access controls → Service credentials → Service Tokens; with `apiToken`, the Cloudflare API token the person made for the run is theirs to delete (dash.cloudflare.com/profile/api-tokens). Revoking, regenerating or changing it stops reviews until a new one is saved on the setup page.
|
|
19
21
|
- **The tool does the writes**: the key as Impeccable's credential for the lock, and the setting that sends it in the host's header (`x-vercel-protection-bypass`, `Authorization`, or `CF-Access-Client-Id` and `CF-Access-Client-Secret`), both through the setup API, so impeccable_write refuses those two. The state's `preview-protection` item words the same calls, and shows `keySaved` once they are done.
|
|
20
22
|
- **A key saved before** (the item shows `keySaved` and a `check` that `passed`): the lock was handled by an earlier run or the person. Run step 2's check and go on; call the tool only when that check fails.
|
|
21
23
|
- **The key is never yours**: it goes from the command's own process to the host and to Impeccable, and into no message, status line, file, command or report. `vercel project protection`, `vercel api`, `vercel curl` and `netlify api` print it, and the fence refuses them.
|
|
@@ -12,7 +12,7 @@ Every run, once the settings are saved. The person merges the setup pull request
|
|
|
12
12
|
- **Netlify** (netlify-previews, netlify-setup) builds a deploy preview only for a pull request: open the test drive now as a **draft**, step 5's `gh pr create` with `--draft`, which Impeccable leaves unreviewed. impeccable_netlify_previews with `pull` the number it printed waits up to five minutes for the deploy preview, under a status line with its timer. Prove the test drive's page on it, save the method as the case says, then `gh pr ready impeccable/test-drive --repo <owner>/<repo>` marks it ready, which starts the review. Then step 5's recording.
|
|
13
13
|
- **Impeccable's own preview** (static-site): the workflow in the branch builds it on the pull request, and Impeccable's review holds up to eight minutes for that job before it renders, so step 3's proof and the workflow in the branch (from the setup commit) are all this step takes. Then step 5.
|
|
14
14
|
|
|
15
|
-
Done when the test drive's page is proven on its preview, and a host's method saved; when nothing proved it, the reference below says how the run goes on.
|
|
15
|
+
A host's wait that ran out shows its own screen, which opens the host's page where its builds are turned on and waits again on the person's yes. Done when the test drive's page is proven on its preview, and a host's method saved; when nothing proved it, the reference below says how the run goes on.
|
|
16
16
|
5. **Open the test drive and record it**: `gh pr create --repo <owner>/<repo> --head impeccable/test-drive --base <defaultBranch>` with the title "Impeccable test drive (safe to close)" and a body that starts "This pull request only shows Impeccable reviewing a change. Close it when you are done; nothing here needs to be merged." and then names the two pages and lists the flaws planted for the review to find, one line each (a Netlify draft opened this way in step 4, and was marked ready there). Record it right away with impeccable_test_pr, so the web page follows its review; it refuses a draft. Done when impeccable_test_pr answered "Recorded".
|
|
17
17
|
6. **Check the test drive's preview once**, while it is the newest open pull request: impeccable_write to `latest-pull-check` with the app's `appKey`. Done when it answered; this early a 409 `no-preview-running` or a job still starting is expected, and the preview goes under "What is not verified".
|
|
18
18
|
7. **Open the setup pull request**, when there is a setup commit: `git push -u origin impeccable/setup`, then `gh pr create --repo <owner>/<repo> --head impeccable/setup --base <defaultBranch>` with the title "Set up Impeccable previews" and a body that starts "Merge this to keep Impeccable's previews working." and then says what it carries and why, one line each. Record it with impeccable_test_pr: the test drive again, and this one in `setup_pr`. Done when impeccable_test_pr answered "Recorded".
|
|
@@ -26,6 +26,6 @@ Every run, once the settings are saved. The person merges the setup pull request
|
|
|
26
26
|
- **Low-contrast text**: text in a solid light grey, well under 4.5:1 against its background.
|
|
27
27
|
- **A layout transition**: `transition: width 0.3s`, naming the layout property.
|
|
28
28
|
- **Body text at the viewport edge**: a paragraph of more than 40 characters in the main content, without a background, flush with the sides of the viewport.
|
|
29
|
-
- **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left`. The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report
|
|
29
|
+
- **No preview, no test drive**: when step 4 ends with the preview unproven (the host's build failed or none came within the five minutes, the host's login stayed in front, step 3 failed), open no test drive: mark the Test drive task failed, say under "What is not verified" what was missing and what the person can do, and put their step in `left`. The setup pull request still opens in step 7 when there is a setup commit. A Netlify draft stays open, unrecorded: name it in the report. Put "Mark the test drive #N ready once Netlify builds its preview" in `left` only where Netlify built no preview; where it built one and the lock stayed in front, preview-protection's step 3 says what the report says instead.
|
|
30
30
|
- **A single-page app**, whose routes live in the browser: the new page is a self-contained HTML file in the folder the build copies as it is (`public/` in Vite), so the output holds it for `entries` and the review counts it as a page added.
|
|
31
31
|
- **A branch name an earlier run took** (checkout or push says so): use `-2`, then `-3`, and name the earlier pull request in the report so the person can close it.
|
|
@@ -8,10 +8,10 @@ The pages need a server from this repository (SSR, API routes, or a backend the
|
|
|
8
8
|
- **Several apps**: ask once for each app this case applies to, the title naming the app (*"Order desk previews"*).
|
|
9
9
|
- **Asked already for this app in this run**, or a host's setup for it ended without a preview (the sign-in skipped, a step failed): ask nothing, and go on as after a skip.
|
|
10
10
|
2. **Set the host up**: on `vercel`, as the vercel-setup case says, and on `netlify`, as the netlify-setup case says. The pick stands in for that case's "When this applies": its steps run whatever the checkout holds for the host, and even where the host already deploys previews that showed another app or did not answer, since the test drive's own preview, proven on that host, decides. Done when that case's steps are done.
|
|
11
|
-
3. **After a skip**: no preview, no test drive. Save no method and write no workflow for these pages, and open no test drive, as pull-requests' "No preview, no test drive" says. Mark the Preview method and Test drive tasks failed, say under "What was skipped, and why" that the pages need a server and no host deploys previews of them, and
|
|
11
|
+
3. **After a skip**: no preview, no test drive. Save no method and write no workflow for these pages, and open no test drive, as pull-requests' "No preview, no test drive" says. Mark the Preview method and Test drive tasks failed, and say under "What was skipped, and why" that the pages need a server and no host deploys previews of them, naming Vercel, Netlify and Cloudflare as the hosts whose pull request previews Impeccable reviews. The checkup's entry for this preview then stays in `next`, which counts as done for the playbook's step 5. With several apps this holds for the skipped app alone: the other chosen apps' previews and the test drive go on as monorepo says, and the tasks fail only where no app has a preview. Done when the report says so.
|
|
12
12
|
|
|
13
13
|
### Reference
|
|
14
14
|
|
|
15
|
-
- **Already on Impeccable's tunnel**: a repository whose state shows `ci-tunnel` as the preview's method, with its workflow committed, keeps working as it is. Change nothing about that preview: a host the person picks here replaces it once its preview is saved, and after a skip the tunnel stays, which the report says
|
|
15
|
+
- **Already on Impeccable's tunnel**: a repository whose state shows `ci-tunnel` as the preview's method, with its workflow committed, keeps working as it is. Change nothing about that preview: a host the person picks here replaces it once its preview is saved, and after a skip the tunnel stays, which the report says.
|
|
16
16
|
- **Never save `ci-tunnel`**: impeccable_write refuses it.
|
|
17
17
|
- The sign-in case applies all the same: the accounts serve the host's previews once they come.
|
|
@@ -10,7 +10,7 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
|
|
|
10
10
|
- **Neither `team` nor `teams`**: the account has no team, and `vercel link --yes` without `--team` links in the person's own scope.
|
|
11
11
|
- **Skip**: set the preview up as server-previews or static-site says, and say under "What was skipped, and why" that Vercel was found and the sign-in was skipped. The steps below do not apply.
|
|
12
12
|
2. **Create the project** (run each Vercel command plain: its answer is short, and a pipe to `tail` is refused): `vercel link --yes --team <the team>` creates the project in that team, named after the folder (or links the one that has that name), and connects the GitHub `origin`; the CLI names no team on its own without a terminal, so the option is given whenever there is a team. Done when it prints the linked project.
|
|
13
|
-
3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, asks the person for Vercel's GitHub app's access when the app cannot reach the repository, and then, on a project with no production deployment, puts Vercel's login in front of every address of the project, its own included. Vercel makes a new project's first deployment Production whatever branch it builds and serves it at the project's own address, and the test drive's push is that first deployment. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
|
|
13
|
+
3. **Connect the repository, and lock the project before its first deployment**, with impeccable_vercel_connect: it runs the connection on the linked project, asks the person for Vercel's GitHub app's access when the app cannot reach the repository, and then, on a project with no production deployment, puts Vercel's login in front of every address of the project, its own included. Vercel makes a new project's first deployment Production whatever branch it builds and serves it at the project's own address, and the test drive's push is that first deployment. When Vercel refuses the lock, the tool asks the person on one screen to turn it on in the project's settings and waits for it. Done when it answered `connected: true` with `production: "exists"` or `locked: true`; a project connected by the link already is, and it says so.
|
|
14
14
|
4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes vercel-previews' reference gives: from the default branch, no setup pull request, built once with plain commands. Stage its files by name: `vercel link` writes `.vercel/` and a `.env.local` that holds a Vercel token, and may add `.vercel` to `.gitignore`, and none of that goes into the pull request. Its push starts the project's first deployment. Done when `git push -u origin impeccable/test-drive` succeeded.
|
|
15
15
|
5. **Its preview, proven and saved, then its pull request** as pull-requests' step 4 (Vercel) and step 5 say: the wait for the pushed head's deployment, the test drive's page proven on the address it lists (past Vercel's login as preview-protection says), impeccable_write to `preview` as vercel-previews' step 2 says, then the pull request and its recording. The playbook's step 6 then has no pull request left to open: go on with the playbook's step 4. Done when impeccable_test_pr answered "Recorded".
|
|
16
16
|
|
|
@@ -18,12 +18,12 @@ The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `ver
|
|
|
18
18
|
|
|
19
19
|
- **A step fails before the test drive is pushed** (the sign-in skipped or not completed, the team skipped, the project not created, the repository not connected): the Vercel setup ends there. Set the preview up as server-previews or static-site says, say under "What was skipped, and why" what stopped it, and put the person's step in `left` when there is one, such as "Give Vercel's GitHub app access to the repository", with the `access` page in the report.
|
|
20
20
|
- **A push before step 3** is refused, since a connected project builds every push: run impeccable_vercel_connect first.
|
|
21
|
-
- **`locked: false`** from step 3, with `production` not "exists": push nothing, and set up no other preview either, since the connected project would build that push as its first deployment, public at its own address. Mark the Test drive task failed, say under "What was skipped, and why" what stopped the lock (the answer's `reason`)
|
|
21
|
+
- **`locked: false`** from step 3, with `production` not "exists" (the person skipped the tool's screen, or the lock was still off after its retries): push nothing, and set up no other preview either, since the connected project would build that push as its first deployment, public at its own address. Mark the Test drive task failed, and say under "What was skipped, and why" what stopped the lock (the answer's `reason`).
|
|
22
22
|
- **No preview for the pushed test drive**: save no method, set up no other preview and open no pull request, as pull-requests' "No preview, no test drive" says; the project and the connection stay, and the report names them.
|
|
23
|
-
- **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed,
|
|
24
|
-
- **No deployment within five minutes**: the
|
|
23
|
+
- **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed. When the cause is in the test drive's own change, correct it, commit, push once more and wait for that build; else the report names the cause.
|
|
24
|
+
- **No deployment within five minutes**: the wait's own screen opens the project's Git settings and waits again on the person's yes. When they skip it, or none came after its retries, say under "What is not verified" that Vercel built no push here, and that the project's Git connection is the thing to check.
|
|
25
25
|
- **Behind Vercel's login** (`gate: "vercel-protection"`) with nothing getting past it: preview-protection says what the report names.
|
|
26
|
-
- **The first deployment** of a new project is called Production, whatever branch it builds. Locked in step 3, and still so when its deployment is read, it is the test drive's preview: impeccable_hosted_previews lists it, and the proof opens it past Vercel's login as preview-protection says. Unlocked by then, or another project's, the evidence lists no address for it and the proof refuses it: no test drive, as "No preview for the pushed test drive" says, and the report says under "What is not verified" that Vercel serves the test drive's build at the project's own address (`vercel project ls` prints it) without a login until the default branch deploys,
|
|
26
|
+
- **The first deployment** of a new project is called Production, whatever branch it builds. Locked in step 3, and still so when its deployment is read, it is the test drive's preview: impeccable_hosted_previews lists it, and the proof opens it past Vercel's login as preview-protection says. Unlocked by then, or another project's, the evidence lists no address for it and the proof refuses it: no test drive, as "No preview for the pushed test drive" says, and the report says under "What is not verified" that Vercel serves the test drive's build at the project's own address (`vercel project ls` prints it) without a login until the default branch deploys, and that Vercel Authentication for All Deployments, under the project's Settings → Deployment Protection, puts its login back in front. A project that deployed production before the run (`production: "exists"`) builds the push as a Preview, and keeps its addresses as they were.
|
|
27
27
|
- **Behind Vercel's login** (`gate: "vercel-protection"`): a project the CLI creates starts with Vercel Authentication on, and step 3 locks it at every address, so expect it, and go on as the preview-protection case says.
|
|
28
28
|
- **The report** names the Vercel project and its team under "What was configured, and by whom", whichever way the run ended, since it stays in the person's Vercel account, and the CLI's install when there was one, and that the link wrote a `.env.local` with a Vercel token in the checkout. A project locked in step 3 gets this too: Vercel's login stands in front of every address of it, its production one included, until the person chooses Standard Protection under Settings → Deployment Protection, which opens production and keeps the previews behind the login. When the answer's `productionBranch` is not the repository's default branch, say under "Before you merge" that Vercel deploys production from that branch (Settings → Environments → Production → Branch Tracking changes it). The project's environment variables are the person's: when the app needs some (`.env.example`), list their names, never their values, under "Before you merge".
|
|
29
29
|
- **Status lines**: the sign-in, the link, the connection and the lock belong to the Preview method task; from the page on, the Test drive task, so name the test drive in each line about its build, its push, the wait for its preview, the proof and the save (*"Saving the test drive's preview as the method"*). Word Vercel's login as a login, since a line about signing in belongs to the Sign-in task.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
const LIMIT = 20;
|
|
5
|
+
const COVERAGE = 'Only the 20 newest GitHub deployments, their latest statuses, and the 20 most recently updated pull requests are checked. A missing match does not prove previews are absent. Empty GitHub deployment history does not prove that a live app or host previews do not exist. A deployment URL does not prove the application rendered.';
|
|
6
|
+
const FAILURE = 'GitHub preview evidence could not be read completely.';
|
|
7
|
+
const STATES = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
|
|
8
|
+
function record(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function positiveInteger(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
15
|
+
}
|
|
16
|
+
function sha(value) {
|
|
17
|
+
return typeof value === 'string' && /^[a-f0-9]{40}$/.test(value);
|
|
18
|
+
}
|
|
19
|
+
function previewUrl(value) {
|
|
20
|
+
if (typeof value !== 'string' || value.length > 2048 || !/^https:\/\/[^\s\\?#]+$/i.test(value))
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(value);
|
|
24
|
+
if (url.username || url.password || url.search || url.hash || /^https:\/\/[^/]*@/i.test(value))
|
|
25
|
+
return null;
|
|
26
|
+
return url.href;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Read a bounded projection of GitHub evidence; never return response bodies or command errors. */
|
|
33
|
+
export async function readHostedPreviews(owner, repo, dependencies = {}) {
|
|
34
|
+
if (!/^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(owner)
|
|
35
|
+
|| !/^[a-zA-Z0-9_.-]{1,100}$/.test(repo) || repo === '.' || repo === '..') {
|
|
36
|
+
return { coverage: COVERAGE, deployments: [], error: FAILURE };
|
|
37
|
+
}
|
|
38
|
+
const runGh = dependencies.runGh ?? (async (args) => {
|
|
39
|
+
const { stdout } = await execFileAsync('gh', args, {
|
|
40
|
+
encoding: 'utf8', timeout: 30_000, maxBuffer: 2 * 1024 * 1024, signal: dependencies.signal,
|
|
41
|
+
});
|
|
42
|
+
return stdout;
|
|
43
|
+
});
|
|
44
|
+
const base = `/repos/${owner}/${repo}`;
|
|
45
|
+
async function get(route) {
|
|
46
|
+
if (dependencies.signal?.aborted)
|
|
47
|
+
throw new Error(FAILURE);
|
|
48
|
+
const stdout = await runGh(['api', '--hostname', 'github.com', '--method', 'GET', route]);
|
|
49
|
+
if (dependencies.signal?.aborted)
|
|
50
|
+
throw new Error(FAILURE);
|
|
51
|
+
const data = JSON.parse(stdout);
|
|
52
|
+
if (!Array.isArray(data))
|
|
53
|
+
throw new Error(FAILURE);
|
|
54
|
+
return data.slice(0, LIMIT);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const [deployments, pulls] = await Promise.all([
|
|
58
|
+
get(`${base}/deployments?per_page=${LIMIT}`),
|
|
59
|
+
get(`${base}/pulls?state=all&sort=updated&direction=desc&per_page=${LIMIT}`),
|
|
60
|
+
]);
|
|
61
|
+
let incomplete = false;
|
|
62
|
+
const evidence = [];
|
|
63
|
+
for (const entry of deployments) {
|
|
64
|
+
if (dependencies.signal?.aborted) {
|
|
65
|
+
incomplete = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
const deployment = record(entry);
|
|
69
|
+
if (!deployment || !positiveInteger(deployment.id) || !sha(deployment.sha))
|
|
70
|
+
continue;
|
|
71
|
+
let latest;
|
|
72
|
+
try {
|
|
73
|
+
latest = record((await get(`${base}/deployments/${deployment.id}/statuses?per_page=1`))[0]);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
incomplete = true;
|
|
77
|
+
if (dependencies.signal?.aborted)
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const status = STATES.find((state) => state === latest?.state) ?? null;
|
|
81
|
+
const environment = typeof deployment.environment === 'string' && deployment.environment.length <= 100
|
|
82
|
+
&& !/[\u0000-\u001f\u007f]/.test(deployment.environment) ? deployment.environment : null;
|
|
83
|
+
const pullRequests = [];
|
|
84
|
+
for (const item of pulls) {
|
|
85
|
+
const pull = record(item);
|
|
86
|
+
if (pull && positiveInteger(pull.number) && (pull.state === 'open' || pull.state === 'closed')
|
|
87
|
+
&& record(pull.head)?.sha === deployment.sha) {
|
|
88
|
+
pullRequests.push({ number: pull.number, state: pull.state });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
evidence.push({ id: deployment.id, sha: deployment.sha, environment, status,
|
|
92
|
+
previewUrl: status ? previewUrl(latest?.environment_url) : null, pullRequests });
|
|
93
|
+
}
|
|
94
|
+
incomplete ||= dependencies.signal?.aborted ?? false;
|
|
95
|
+
return { coverage: COVERAGE, deployments: evidence,
|
|
96
|
+
...(incomplete ? { error: FAILURE } : { emptyDeploymentHistory: deployments.length === 0 }) };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { coverage: COVERAGE, deployments: [], error: FAILURE };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Keep Netlify's repository-wide pattern tied to successful discovery evidence. */
|
|
2
|
+
export function netlifyPatternError(body, observed) {
|
|
3
|
+
const pattern = body.previewUrlPattern;
|
|
4
|
+
if (typeof pattern !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
let netlify = /netlify\.app|<site>/i.test(pattern);
|
|
7
|
+
try {
|
|
8
|
+
const hostname = new URL(pattern.replaceAll('{pr}', '1')).hostname;
|
|
9
|
+
netlify ||= hostname === 'netlify.app' || hostname.endsWith('.netlify.app');
|
|
10
|
+
}
|
|
11
|
+
catch { /* The canonical validation below rejects malformed Netlify patterns. */ }
|
|
12
|
+
if (!netlify)
|
|
13
|
+
return null;
|
|
14
|
+
const match = /^https:\/\/deploy-preview-\{pr\}--([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\.netlify\.app$/.exec(pattern);
|
|
15
|
+
if (!match || match[1].length > 63)
|
|
16
|
+
return 'Use a real Netlify site pattern from successful preview evidence; placeholders and malformed patterns are not saved.';
|
|
17
|
+
if (Object.hasOwn(body, 'appKey'))
|
|
18
|
+
return 'A Netlify preview URL pattern belongs to the repository; omit appKey.';
|
|
19
|
+
if (body.intendedMethod !== 'preview-deploy')
|
|
20
|
+
return 'Save a Netlify pattern with intendedMethod preview-deploy.';
|
|
21
|
+
if (!observed.has(pattern))
|
|
22
|
+
return 'Read this repository\'s Netlify previews first and choose a pattern backed by a successful current-head status.';
|
|
23
|
+
return null;
|
|
24
|
+
}
|