@impeccable/reviews 0.1.0
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 +68 -0
- package/dist/agent.js +183 -0
- package/dist/bin.js +11 -0
- package/dist/fence.js +1071 -0
- package/dist/focus.js +98 -0
- package/dist/host-questions.js +71 -0
- package/dist/hosted.js +195 -0
- package/dist/http.js +36 -0
- package/dist/log.js +38 -0
- package/dist/login.js +276 -0
- package/dist/main.js +252 -0
- package/dist/netlify.js +333 -0
- package/dist/playbook/cases/ci-tunnel.md +21 -0
- package/dist/playbook/cases/first-preview-failure.md +28 -0
- package/dist/playbook/cases/netlify-previews.md +17 -0
- package/dist/playbook/cases/netlify-setup.md +24 -0
- package/dist/playbook/cases/preview-protection.md +21 -0
- package/dist/playbook/cases/pull-requests.md +31 -0
- package/dist/playbook/cases/sign-in.md +21 -0
- package/dist/playbook/cases/static-site.md +16 -0
- package/dist/playbook/cases/vercel-previews.md +16 -0
- package/dist/playbook/cases/vercel-setup.md +27 -0
- package/dist/playbook/cases/workflow-as-person.md +23 -0
- 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 +98 -0
- package/dist/preflight.js +211 -0
- package/dist/stages.js +34 -0
- package/dist/sync.js +142 -0
- package/dist/tools.js +2015 -0
- package/dist/transcript.js +139 -0
- package/dist/ui/bridge.js +145 -0
- package/dist/ui/frames.js +62 -0
- package/dist/ui/look.js +368 -0
- package/dist/ui/run-state.js +91 -0
- package/dist/ui/screens.js +544 -0
- package/dist/ui/sensitive.js +14 -0
- package/dist/vercel-bypass.js +136 -0
- package/dist/vercel.js +70 -0
- package/package.json +39 -0
package/dist/netlify.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
/**
|
|
7
|
+
* Netlify: the deploy previews this repository's pull requests get (from
|
|
8
|
+
* GitHub's record and from the Netlify CLI on this machine when it is signed
|
|
9
|
+
* in), whether that CLI is signed in, and the connection of a site to this
|
|
10
|
+
* repository through Netlify's site settings and its GitHub app. Nothing here
|
|
11
|
+
* reads a token, and nothing comes back beyond the fields below.
|
|
12
|
+
*/
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
/** How many pull requests, the most recently updated, the evidence reads. */
|
|
15
|
+
const NEWEST = 10;
|
|
16
|
+
const SITE = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
|
|
17
|
+
/** The commit status Netlify reports on a pull request's head. */
|
|
18
|
+
const STATUS = new RegExp(`^netlify/(${SITE})/deploy-preview$`);
|
|
19
|
+
/** A deploy preview's address, as its bot links it. */
|
|
20
|
+
const LINK = new RegExp(`^deploy-preview-([1-9]\\d{0,6})--(${SITE})\\.netlify\\.app$`);
|
|
21
|
+
const STATES = ['error', 'failure', 'pending', 'success'];
|
|
22
|
+
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined);
|
|
23
|
+
const positive = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
24
|
+
const when = (value) => (typeof value === 'string' && Number.isFinite(Date.parse(value)) ? Date.parse(value) : 0);
|
|
25
|
+
const list = (value) => (Array.isArray(value) ? value.map(record).filter((item) => item !== undefined) : []);
|
|
26
|
+
/** The address Netlify serves a pull request's preview at, when the name fits a host name's 63 characters. */
|
|
27
|
+
const previewAddress = (pullRequest, site) => (`deploy-preview-${pullRequest}--${site}`.length <= 63 ? `https://deploy-preview-${pullRequest}--${site}.netlify.app` : null);
|
|
28
|
+
/**
|
|
29
|
+
* A runner for gh or netlify here: its input ends at once, so nothing waits on it, and no browser opens (a sign-in is the
|
|
30
|
+
* person's) unless `browser` says so, for `netlify login`, which opens one and waits for the person's approval there.
|
|
31
|
+
*/
|
|
32
|
+
export function commandRunner(name, cwd, opts = {}) {
|
|
33
|
+
return async (args) => {
|
|
34
|
+
try {
|
|
35
|
+
const env = { ...process.env, ...(opts.browser ? {} : { BROWSER: 'none', CI: 'true' }), GH_PROMPT_DISABLED: '1' };
|
|
36
|
+
const run = execFileAsync(name, args, { cwd, encoding: 'utf8', timeout: opts.timeoutMs ?? 30_000, maxBuffer: 16 * 1024 * 1024, signal: opts.signal, env });
|
|
37
|
+
run.child.stdin?.end();
|
|
38
|
+
return (await run).stdout;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const { stdout, stderr, code } = err;
|
|
42
|
+
throw Object.assign(new Error(`${name} failed`), { stdout, stderr, missing: code === 'ENOENT' });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** `netlify status --json`: whether the CLI is signed in, and the names of the teams the account lists. Signed out, or signed in but linked to no site, it answers on stdout and exits 1. */
|
|
47
|
+
export async function netlifyStatus(netlify) {
|
|
48
|
+
let status;
|
|
49
|
+
try {
|
|
50
|
+
status = record(JSON.parse(await netlify(['status', '--json'])));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err.missing)
|
|
54
|
+
return { cli: 'missing', teams: [] };
|
|
55
|
+
try {
|
|
56
|
+
status = record(JSON.parse(String(err.stdout)));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { cli: 'unknown', teams: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const teams = record(status?.account)?.Teams?.filter((team) => typeof team === 'string' && team.length <= 100) ?? [];
|
|
63
|
+
return { cli: status?.loggedIn === true ? 'signed-in' : 'signed-out', teams };
|
|
64
|
+
}
|
|
65
|
+
/** The person's Netlify teams: each one's name, and its slug, which is what `sites:create --account-slug` takes. */
|
|
66
|
+
export async function netlifyTeams(netlify) {
|
|
67
|
+
try {
|
|
68
|
+
return list(JSON.parse(await netlify(['api', 'listAccountsForUser'])))
|
|
69
|
+
.filter((team) => typeof team.slug === 'string' && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,99}$/.test(team.slug) && typeof team.name === 'string' && team.name.length <= 100)
|
|
70
|
+
.map((team) => ({ label: team.name, value: team.slug }));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** What GitHub records: each recent pull request's Netlify commit status, and the preview its Netlify bot linked; or, with `only`, that pull request's. `failed` when a read did not answer. */
|
|
77
|
+
async function fromGitHub(owner, repo, gh, only) {
|
|
78
|
+
const base = `/repos/${owner}/${repo}`;
|
|
79
|
+
const get = async (route) => JSON.parse(await gh(['api', '--hostname', 'github.com', '--method', 'GET', route]));
|
|
80
|
+
const all = async (route) => {
|
|
81
|
+
const data = await get(route);
|
|
82
|
+
if (!Array.isArray(data))
|
|
83
|
+
throw new Error(`${route} is not a list`);
|
|
84
|
+
return list(data);
|
|
85
|
+
};
|
|
86
|
+
let failed = false;
|
|
87
|
+
const read = (what) => what.catch(() => ((failed = true), []));
|
|
88
|
+
const [pulls, comments] = await Promise.all([
|
|
89
|
+
read(only ? get(`${base}/pulls/${only}`).then((one) => list([one])) : all(`${base}/pulls?state=all&sort=updated&direction=desc&per_page=${NEWEST}`)),
|
|
90
|
+
read(all(`${base}/issues/comments?sort=updated&direction=desc&per_page=100`)),
|
|
91
|
+
]);
|
|
92
|
+
const heads = pulls.flatMap((pull) => { const sha = record(pull.head)?.sha; return positive(pull.number) && (!only || pull.number === only) && typeof sha === 'string' && /^[0-9a-f]{40}$/.test(sha) ? [{ number: pull.number, sha, at: when(pull.updated_at) }] : []; });
|
|
93
|
+
const statuses = await Promise.all(heads.map(({ sha }) => read(all(`${base}/commits/${sha}/statuses?per_page=100`))));
|
|
94
|
+
const found = [];
|
|
95
|
+
const add = (pullRequest, site, state, from, at) => {
|
|
96
|
+
const url = previewAddress(pullRequest, site);
|
|
97
|
+
// GitHub lists a commit's statuses newest first: the first of a pull request's site is its current one.
|
|
98
|
+
if (url && !found.some((p) => p.pullRequest === pullRequest && p.site === site))
|
|
99
|
+
found.push({ pullRequest, site, url, state, from, at });
|
|
100
|
+
};
|
|
101
|
+
statuses.forEach((all, at) => {
|
|
102
|
+
for (const status of all) {
|
|
103
|
+
const site = typeof status.context === 'string' ? STATUS.exec(status.context)?.[1] : undefined;
|
|
104
|
+
if (site)
|
|
105
|
+
add(heads[at].number, site, STATES.find((s) => s === status.state) ?? null, 'commit status', when(status.updated_at) || heads[at].at);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
for (const comment of comments) {
|
|
109
|
+
const issue = typeof comment.issue_url === 'string' ? Number(/\/issues\/([1-9]\d*)$/.exec(comment.issue_url)?.[1]) : NaN;
|
|
110
|
+
if (record(comment.user)?.login !== 'netlify[bot]' || !Number.isSafeInteger(issue) || (only && issue !== only) || typeof comment.body !== 'string')
|
|
111
|
+
continue;
|
|
112
|
+
for (const [link] of comment.body.matchAll(/https:\/\/deploy-preview-[\w-]+\.netlify\.app/g)) {
|
|
113
|
+
const [, number, site] = LINK.exec(new URL(link).hostname) ?? [];
|
|
114
|
+
if (site && Number(number) === issue)
|
|
115
|
+
add(issue, site, 'ready', 'bot comment', when(comment.updated_at));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { previews: found, heads: new Map(heads.map((head) => [head.number, head.sha])), failed };
|
|
119
|
+
}
|
|
120
|
+
/** What the Netlify CLI on this machine knows, when it is signed in: the sites built from this repository, and their deploy previews. */
|
|
121
|
+
async function fromCli(owner, repo, netlify, only) {
|
|
122
|
+
const { cli } = await netlifyStatus(netlify);
|
|
123
|
+
if (cli !== 'signed-in')
|
|
124
|
+
return { cli, sites: [], previews: [], failed: cli === 'unknown' };
|
|
125
|
+
const slug = `${owner}/${repo}`.toLowerCase();
|
|
126
|
+
const sites = [];
|
|
127
|
+
const previews = [];
|
|
128
|
+
let failed = false;
|
|
129
|
+
try {
|
|
130
|
+
for (const site of list(JSON.parse(await netlify(['sites:list', '--json'])))) {
|
|
131
|
+
const path = record(site.build_settings)?.repo_path;
|
|
132
|
+
// A site is this repository's only when it is built from it: the folder's own link to a site says nothing of that.
|
|
133
|
+
if (typeof site.name !== 'string' || typeof site.id !== 'string' || !new RegExp(`^${SITE}$`).test(site.name) || typeof path !== 'string' || path.toLowerCase() !== slug)
|
|
134
|
+
continue;
|
|
135
|
+
sites.push(site.name);
|
|
136
|
+
try {
|
|
137
|
+
// The newest 100 deploy previews, newest first: the first deploy of a pull request is its current one.
|
|
138
|
+
for (const deploy of list(JSON.parse(await netlify(['api', 'listSiteDeploys', '--data', JSON.stringify({ site_id: site.id, 'deploy-previews': true, per_page: 100 })])))) {
|
|
139
|
+
const url = positive(deploy.review_id) ? previewAddress(deploy.review_id, site.name) : null;
|
|
140
|
+
if (deploy.context !== 'deploy-preview' || !url || (only && deploy.review_id !== only) || previews.some((p) => p.pullRequest === deploy.review_id && p.site === site.name))
|
|
141
|
+
continue;
|
|
142
|
+
previews.push({ pullRequest: deploy.review_id, site: site.name, url, state: typeof deploy.state === 'string' && /^[a-z_]{1,20}$/.test(deploy.state) ? deploy.state : null, from: 'netlify cli', at: when(deploy.updated_at) || when(deploy.created_at), ...(typeof deploy.commit_ref === 'string' && /^[0-9a-f]{40}$/.test(deploy.commit_ref) ? { sha: deploy.commit_ref } : {}) });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
failed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
failed = true;
|
|
152
|
+
}
|
|
153
|
+
return { cli: 'signed-in', sites, previews, failed };
|
|
154
|
+
}
|
|
155
|
+
/** Whether this repository's pull requests get Netlify deploy previews: the sites and the newest previews that GitHub and the signed-in Netlify CLI name; or, with `pull`, that pull request's only. */
|
|
156
|
+
export async function readNetlifyPreviews(owner, repo, cwd, deps = {}) {
|
|
157
|
+
if (!/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(owner) || !/^[A-Za-z0-9_.-]{1,100}$/.test(repo) || repo === '.' || repo === '..') {
|
|
158
|
+
return { sites: [], previews: [], cli: 'missing', error: "Netlify's previews could not be read." };
|
|
159
|
+
}
|
|
160
|
+
const [github, cli] = await Promise.all([fromGitHub(owner, repo, deps.gh ?? commandRunner('gh', cwd, { signal: deps.signal }), deps.pull), fromCli(owner, repo, deps.netlify ?? commandRunner('netlify', cwd, { signal: deps.signal }), deps.pull)]);
|
|
161
|
+
// A deploy of an older commit than its pull request's head says nothing of the head's own preview: a commit status, which names the head, outranks it, and a wait for that pull request's preview ignores it.
|
|
162
|
+
const older = (p) => p.from === 'netlify cli' && Boolean(p.sha) && github.heads.has(p.pullRequest) && github.heads.get(p.pullRequest) !== p.sha;
|
|
163
|
+
// Of a pull request's status and the CLI's deploy of the same commit the later change is the truer, a status winning a tie; an older commit's deploy comes next, a bot's comment, which names no commit and changes no state, last.
|
|
164
|
+
const weight = (p) => (p.from === 'bot comment' ? 2 : older(p) ? 1 : 0);
|
|
165
|
+
const ranked = [...github.previews, ...cli.previews].filter((p) => !(deps.pull && older(p))).sort((a, b) => weight(a) - weight(b) || b.at - a.at);
|
|
166
|
+
const previews = [];
|
|
167
|
+
for (const p of ranked)
|
|
168
|
+
if (!previews.some((q) => q.pullRequest === p.pullRequest && q.site === p.site))
|
|
169
|
+
previews.push(p);
|
|
170
|
+
// The most recently updated first: an old pull request with a high number is not the newest preview.
|
|
171
|
+
previews.sort((a, b) => b.at - a.at);
|
|
172
|
+
return {
|
|
173
|
+
sites: [...new Set([...cli.sites, ...previews.map((p) => p.site)])],
|
|
174
|
+
previews: previews.slice(0, NEWEST).map(({ at, sha, ...preview }) => preview),
|
|
175
|
+
cli: cli.cli,
|
|
176
|
+
...(github.failed || cli.failed ? { error: "Some of Netlify's previews could not be read: what is missing is unknown, not absent." } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const CLONE_WAIT = { totalMs: 60_000, everyMs: 5_000 };
|
|
180
|
+
/** A deploy that has not tried to read the repository yet. */
|
|
181
|
+
const NOT_YET = new Set(['new', 'pending_review', 'accepted', 'enqueued', 'preparing']);
|
|
182
|
+
/** Netlify's word for a build that failed to read the repository: every clone failure, whatever git said, is in the stage that prepares it. */
|
|
183
|
+
const CLONE_FAILED = /preparing repo|access(ing)? (the )?repository|repository not found|could not read from remote/i;
|
|
184
|
+
/** A pause that the run's stop cuts short: false when it did. */
|
|
185
|
+
async function pause(ms, deps) {
|
|
186
|
+
try {
|
|
187
|
+
await (deps.sleep ?? ((wait) => sleep(wait, undefined, { signal: deps.signal })))(ms);
|
|
188
|
+
return !deps.signal?.aborted;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** The site this folder is linked to, as `netlify link` and `sites:create` leave it in .netlify/state.json. */
|
|
195
|
+
async function linkedSiteId(cwd) {
|
|
196
|
+
try {
|
|
197
|
+
const id = JSON.parse(await readFile(join(cwd, '.netlify', 'state.json'), 'utf8')).siteId;
|
|
198
|
+
return typeof id === 'string' && /^[A-Za-z0-9-]{1,64}$/.test(id) ? id : undefined;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const api = async (netlify, method, data) => record(JSON.parse(await netlify(['api', method, '--data', JSON.stringify(data)])));
|
|
205
|
+
/**
|
|
206
|
+
* Whether Netlify's GitHub app reads the repository: what the newest production build since `since` says. A build that
|
|
207
|
+
* failed preparing the repository could not read it; one past that stage could. Unknown when no build has said by
|
|
208
|
+
* the end of the wait (or the run stopped), which leaves the test drive's own preview to tell.
|
|
209
|
+
*/
|
|
210
|
+
export async function netlifyReadsRepository(siteId, since, deps) {
|
|
211
|
+
const now = deps.now ?? Date.now;
|
|
212
|
+
const wait = deps.wait ?? CLONE_WAIT;
|
|
213
|
+
for (const end = now() + wait.totalMs;;) {
|
|
214
|
+
let builds = [];
|
|
215
|
+
try {
|
|
216
|
+
builds = list(JSON.parse(await deps.netlify(['api', 'listSiteDeploys', '--data', JSON.stringify({ site_id: siteId, per_page: 5 })])));
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return 'unknown';
|
|
220
|
+
}
|
|
221
|
+
const build = builds.find((deploy) => deploy.context === 'production' && when(deploy.created_at) >= since - 1000);
|
|
222
|
+
const state = typeof build?.state === 'string' ? build.state : undefined;
|
|
223
|
+
if (state && !NOT_YET.has(state))
|
|
224
|
+
return state === 'error' && CLONE_FAILED.test(String(build.error_message ?? '')) ? 'no' : 'yes';
|
|
225
|
+
if (now() >= end || !(await pause(wait.everyMs, deps)))
|
|
226
|
+
return 'unknown';
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Starts a build of the production branch and says whether Netlify's GitHub app could read the repository for it, trying again every `wait.everyMs` until `wait.totalMs` has passed (or the run stopped): the person is giving the app access meanwhile. */
|
|
230
|
+
export async function waitForNetlifyAccess(siteId, deps) {
|
|
231
|
+
const now = deps.now ?? Date.now;
|
|
232
|
+
for (const end = now() + deps.wait.totalMs; !deps.signal?.aborted;) {
|
|
233
|
+
const since = now();
|
|
234
|
+
try {
|
|
235
|
+
await api(deps.netlify, 'createSiteBuild', { site_id: siteId });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if ((await netlifyReadsRepository(siteId, since, { ...deps, wait: deps.clone })) === 'yes')
|
|
241
|
+
return true;
|
|
242
|
+
if (now() >= end || !(await pause(deps.wait.everyMs, deps)))
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Connects the site this folder is linked to with the repository through Netlify's site settings, as the Netlify UI does
|
|
249
|
+
* when a repository is linked: the repository, its default branch, and the installation of Netlify's GitHub app on the
|
|
250
|
+
* repository's GitHub account, which Netlify names on every site of the person's that the app already builds from that
|
|
251
|
+
* account. GitHub sends the app each push and pull request, so the pull requests build deploy previews. Then the first
|
|
252
|
+
* build says whether the app reads the repository.
|
|
253
|
+
*/
|
|
254
|
+
export async function connectNetlifySite(owner, repo, cwd, deps) {
|
|
255
|
+
const not = (reason, more = {}) => ({ connected: false, reason, ...more });
|
|
256
|
+
const siteId = await linkedSiteId(cwd);
|
|
257
|
+
if (!siteId)
|
|
258
|
+
return not('This folder is linked to no Netlify site: link or create one first.');
|
|
259
|
+
let site;
|
|
260
|
+
try {
|
|
261
|
+
site = await api(deps.netlify, 'getSite', { site_id: siteId });
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return not('Netlify did not answer for the linked site.');
|
|
265
|
+
}
|
|
266
|
+
const name = typeof site?.name === 'string' && new RegExp(`^${SITE}$`).test(site.name) ? site.name : undefined;
|
|
267
|
+
if (!site || !name)
|
|
268
|
+
return not('The linked site could not be read.');
|
|
269
|
+
const linked = { id: siteId, name };
|
|
270
|
+
const slug = `${owner}/${repo}`.toLowerCase();
|
|
271
|
+
const settings = record(site.build_settings);
|
|
272
|
+
let github;
|
|
273
|
+
try {
|
|
274
|
+
github = record(JSON.parse(await deps.gh(['api', '--hostname', 'github.com', '--method', 'GET', `/repos/${owner}/${repo}`])));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
github = undefined;
|
|
278
|
+
}
|
|
279
|
+
/** Where the person gives the app's installation access to the repository: on their own account, or on the organization that owns it. */
|
|
280
|
+
const accessPage = (installation) => (record(github?.owner)?.type === 'Organization' ? `https://github.com/organizations/${owner}/settings/installations/${installation}` : `https://github.com/settings/installations/${installation}`);
|
|
281
|
+
if (typeof settings?.repo_path === 'string' && settings.repo_path.toLowerCase() === slug) {
|
|
282
|
+
// Connected before this run: its newest build says whether the app still reads the repository.
|
|
283
|
+
const reads = await netlifyReadsRepository(siteId, 0, { ...deps, wait: { totalMs: 0, everyMs: 0 } });
|
|
284
|
+
return reads === 'no' && positive(settings.installation_id)
|
|
285
|
+
? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'already', access: accessPage(settings.installation_id) })
|
|
286
|
+
: { connected: true, site: linked, how: 'already' };
|
|
287
|
+
}
|
|
288
|
+
const branch = github?.default_branch;
|
|
289
|
+
const fullName = typeof github?.full_name === 'string' && github.full_name.toLowerCase() === slug ? github.full_name : undefined;
|
|
290
|
+
if (!github || !positive(github.id) || typeof branch !== 'string' || !branch || !fullName)
|
|
291
|
+
return not('GitHub did not answer for the repository.', { site: linked });
|
|
292
|
+
// The app's installation on this GitHub account, as another site of the person's built from it names it.
|
|
293
|
+
let installation;
|
|
294
|
+
try {
|
|
295
|
+
for (const other of list(JSON.parse(await deps.netlify(['sites:list', '--json'])))) {
|
|
296
|
+
const build = record(other.build_settings);
|
|
297
|
+
const path = typeof build?.repo_path === 'string' ? build.repo_path : '';
|
|
298
|
+
if (build?.provider === 'github' && path.toLowerCase().startsWith(`${owner.toLowerCase()}/`) && positive(build.installation_id)) {
|
|
299
|
+
installation = build.installation_id;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return not("The person's Netlify sites could not be read.", { site: linked });
|
|
306
|
+
}
|
|
307
|
+
if (!installation)
|
|
308
|
+
return not(`Netlify's GitHub app is not installed for ${owner}, as far as Netlify's records show: no site of the person's is built from a repository of ${owner}.`, { site: linked });
|
|
309
|
+
const body = {
|
|
310
|
+
provider: 'github',
|
|
311
|
+
id: github.id,
|
|
312
|
+
repo_path: fullName,
|
|
313
|
+
repo_branch: branch,
|
|
314
|
+
allowed_branches: [branch],
|
|
315
|
+
installation_id: installation,
|
|
316
|
+
public_repo: github.private !== true,
|
|
317
|
+
...(deps.settings?.command ? { cmd: deps.settings.command } : {}),
|
|
318
|
+
...(deps.settings?.publish ? { dir: deps.settings.publish } : {}),
|
|
319
|
+
};
|
|
320
|
+
const since = (deps.now ?? Date.now)();
|
|
321
|
+
let updated;
|
|
322
|
+
try {
|
|
323
|
+
updated = await api(deps.netlify, 'updateSite', { site_id: siteId, body: { repo: body } });
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return not('Netlify refused the connection.', { site: linked });
|
|
327
|
+
}
|
|
328
|
+
if (record(updated?.build_settings)?.repo_path !== fullName)
|
|
329
|
+
return not('Netlify did not keep the repository in the site settings.', { site: linked });
|
|
330
|
+
return (await netlifyReadsRepository(siteId, since, deps)) === 'no'
|
|
331
|
+
? not("Netlify's GitHub app cannot read the repository.", { site: linked, how: 'github-app', access: accessPage(installation) })
|
|
332
|
+
: { connected: true, site: linked, how: 'github-app' };
|
|
333
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
The pages need a server from this repository: SSR, API routes, or a backend the front end calls. A front end whose bundle builds on its own still needs its backend here.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Work out the commands** in the workflow's order: install, prepare, build, seed and start, each with the folder it runs in. Done when each has a command, or none, and a folder.
|
|
8
|
+
2. **Prove them** with impeccable_verify_preview in server mode. Done when it answers `verified: true`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof.
|
|
9
|
+
3. **Save them** with impeccable_write to `preview`: `intendedMethod: "ci-tunnel"` and the settings you proved, from the keys `buildSettings` lists. Done when the answer's `keys` show each key stored or unchanged.
|
|
10
|
+
|
|
11
|
+
### Reference
|
|
12
|
+
|
|
13
|
+
- **Seed and migrate only a throwaway database**, never a shared or production one:
|
|
14
|
+
- **SQLite**: a relative file URL in `env` (`DATABASE_URL=file:./preview.db`), and the repository's schema setup as the prepare or seed step.
|
|
15
|
+
- **Postgres or MySQL**: `service`, which gives the steps their own `DATABASE_URL`.
|
|
16
|
+
- **Cloudflare D1**: `--local` migrations and seeds.
|
|
17
|
+
- **The tunnel host refused** (localhost answers, the tunnel host gets a 403): allow the preview host in the framework's host check (Rails `config.hosts`, Django `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS`, Vite `preview.allowedHosts`) for previews only, in the setup pull request; keep the check on.
|
|
18
|
+
- **Readiness**: the job waits for `/` to answer. When only a base path answers, report it as a blocker: no setting points it elsewhere.
|
|
19
|
+
- **Runtime**: compare `node --version` (or python, ruby, pnpm) with the repository's pins; the job sets its runtime from `runtimeVersion`, so a mismatch goes under "What is not verified".
|
|
20
|
+
- **Env**: literal, non-secret values. A secret the app needs is `${{ secrets.NAME }}` in the saved `env`, and "Add the NAME repository secret" in `left`. A demo or test-login flag belongs in the preview's env only.
|
|
21
|
+
- A verified result covers HTTP answers only: rendering and the CI runtime go under "What is not verified".
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
A rerun: the setup pull request an earlier run opened has a preview job that failed, or passed without a usable preview. These steps replace the playbook's steps 3 to 7; stay on that setup branch and pull request.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Record** the earlier pull requests with impeccable_test_pr: the open test drive, and the setup pull request in `setup_pr`. That binding cannot change. Done when it answers "Recorded".
|
|
8
|
+
2. **Read the attempts** with impeccable_preview_run. Done when you know the failed attempt's `stage` and `reason`, the `budget`, and that the state's `lastRun` for the app is the same run.
|
|
9
|
+
3. **Reproduce and correct**: run the failed step again with impeccable_verify_preview and find the smallest correction (see the table). Done when the correction passes locally, or no safe one exists: then push nothing more, and report.
|
|
10
|
+
4. **Push one correction**: save it with impeccable_write to `preview` (with `appKey`), impeccable_sync_workflow, commit on the setup branch, push once, then impeccable_preview_run again. Done when the new attempt shows in `budget`.
|
|
11
|
+
5. **Check it once** with impeccable_write to `latest-pull-check`: `verified: true` means this setup pull request's preview answered at the pushed head, which a passed job alone does not prove. A 409 `no-preview-running` or a job still starting is expected. Done when it answered; report the outcome you saw.
|
|
12
|
+
6. **Open a new test drive** from the corrected setup branch, with the default branch as its base (the earlier one ran on the old head), record it with impeccable_test_pr beside the same setup pull request, then impeccable_report. Done when impeccable_report answered.
|
|
13
|
+
|
|
14
|
+
### Reference
|
|
15
|
+
|
|
16
|
+
- **The budget** is three attempts: the first push and two corrections. The fence counts every push, failed and cancelled ones too, and reruns someone else started count as well; reading attempts is free. When `historyKnown` is false, read the attempts again before pushing. **After the third failure** push nothing and open no test drive: report the last error and the next action.
|
|
17
|
+
- Read a failure only through impeccable_preview_run, which gives a fixed reason and the run link, never log text.
|
|
18
|
+
- impeccable_report appends the attempts and the push count. In your prose, list every correction, including changes to the repository, the last cause and its run link.
|
|
19
|
+
|
|
20
|
+
| Failed step | What to check, and the smallest correction |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| setup | `runtime` in the state; correct `runtimeVersion`. Nothing was installed yet. |
|
|
23
|
+
| install | The lockfile and the package manager's pinned version. |
|
|
24
|
+
| prepare, build | The script's name, the package runner and the app's folder. |
|
|
25
|
+
| upload | The real build output and its entry page; correct `outputDir`. |
|
|
26
|
+
| seed | The migration or seed command and the throwaway database. |
|
|
27
|
+
| start | `startCommand`, `port`, `env` and readiness. |
|
|
28
|
+
| tunnel | The app refusing the forwarded Host (see ci-tunnel), not Impeccable's own access gate: a 403 on the bare link alone is not a broken preview. |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
Netlify deploys previews of the pull requests: impeccable_netlify_previews lists a built preview of a pull request (`state` success or ready), with its address. Ask it only where the repository is set up for Netlify (`netlify.toml`, `.netlify/`, `netlify-cli` or an `@netlify/*` package), but its record decides, not the checkout. Netlify builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Prove the newest preview** with impeccable_prove_preview: the address of the newest listed preview whose state is success or ready (or a page on it a visitor opens without signing in), and words the page shows that the checkout holds, such as the app's name in its title. Done when it answers `proven: true`; when an address no longer answers, try the next newest, three at most.
|
|
8
|
+
2. **Save the method** with impeccable_write to `preview`: `{"previewUrlPattern": "<the proof's pattern>", "intendedMethod": "preview-deploy"}`. The proof stands in for the person's answer, so ask nothing. Done when the answer's `keys` show both stored or unchanged.
|
|
9
|
+
|
|
10
|
+
### Reference
|
|
11
|
+
|
|
12
|
+
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): a password, a team login or Netlify Identity stands in front of the previews, and getting past it is not part of this setup. Save no method and set up no other preview, whatever the checkup's `next` says, and end the report's "Not part of this setup" line with "A password or login in front of the Netlify previews".
|
|
13
|
+
- **Another app, or no answer from any of the three**: Netlify's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Netlify address answered.
|
|
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 ci-tunnel 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
|
+
- **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.
|
|
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".
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
The checkout is set up for Netlify: a `netlify.toml`, a `.netlify/` folder, or a package script that runs the `netlify` command (a Netlify package alone, `@netlify/*` or `netlify-cli`, says nothing of where the repository deploys). And no pull request has a Netlify deploy preview: impeccable_netlify_previews lists none, and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in netlify-previews, the repository needs no workflow and no setup pull request.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "netlify-setup"`. The tool does the rest: it shows the sign-in screen only when the Netlify CLI is signed out (a person signed in already is asked nothing), runs the login in the browser and offers a retry. Done when it answered `confirmed`.
|
|
8
|
+
- **`cli: "missing"`**: install the CLI with the person's package manager (`npm install -g netlify-cli`, `pnpm add -g netlify-cli`, `yarn global add netlify-cli` or `bun add -g netlify-cli`), then ask again: the screen shows no second time.
|
|
9
|
+
- **`teams` in the answer** (the person has several Netlify teams): ask once which owns the site, `kind: "single"`, `id: "netlify-team"`, `options` the teams as given (the tool words the rest), and pass the pick to `sites:create` as `--account-slug`.
|
|
10
|
+
- **Skip**: set the preview up as ci-tunnel or static-site says, and say under "What was skipped, and why" that Netlify was found and the sign-in was skipped. The steps below do not apply.
|
|
11
|
+
2. **Link or create the site**: `netlify link --git-remote-url <the GitHub origin>` links a site Netlify already builds from this repository; when it finds none, `netlify sites:create --name <name>` creates one in the person's team and links this folder. Name it after the repository: lowercase letters, digits and hyphens, at most 40 characters; a name Netlify says is taken gets `-2`. Done when `netlify status` names the linked project.
|
|
12
|
+
3. **Connect the repository** with impeccable_netlify_connect: it connects the linked site to this repository through Netlify's GitHub app, so pull requests build deploy previews, and asks the person for the app's access when Netlify cannot read the repository. Give `command` and `publish` only where no `netlify.toml` sets the build. Done when it answered `connected: true`; a site `netlify link` found is connected already, and it says so.
|
|
13
|
+
4. **Build and push the test drive** (the Test drive task) as pull-requests' steps 2 to 4 say, with the changes netlify-previews' reference gives: from the default branch, no setup pull request, built once with the build Netlify runs. Stage its files by name: `netlify sites:create` writes `.netlify/` and may add it to `.gitignore`, and that change stays out of the pull request. Done when `git push -u origin impeccable/test-drive` succeeded.
|
|
14
|
+
5. **Its draft, its preview proven and saved, then marked ready** as pull-requests' step 4 (Netlify) and step 5 say: the draft, whose deploy preview is the site's first, the wait for it by `pull`, the test drive's page proven on it, impeccable_write to `preview` as netlify-previews' step 2 says, `gh pr ready`, then the 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".
|
|
15
|
+
|
|
16
|
+
### Reference
|
|
17
|
+
|
|
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 ci-tunnel 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
|
+
- **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
|
+
- **The build failed** (`state` is `error` or `failure`): `netlify deploy --build --context deploy-preview` builds this linked checkout as Netlify does, uploads a draft, and prints why it failed, so the report can name the cause.
|
|
21
|
+
- **No deploy preview within five minutes**: the build had not finished, or Netlify builds no deploy preview for pull requests here: Deploy Previews may be off in the site's settings, so put "Turn Deploy Previews on in the site's Netlify settings" in `left`.
|
|
22
|
+
- **Behind a password or Netlify's login** (`gate: "netlify-protection"`): handle it as netlify-previews says.
|
|
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
|
+
- **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.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
impeccable_prove_preview answered `gate: "vercel-protection"`: Vercel's Deployment Protection (Vercel Authentication, or Password Protection, which shows the same gate) stands in front of a preview that Vercel made, and a visitor gets its login. Vercel's way in for a tool is a bypass secret of the project, sent in a header, which every plan has. A project the Vercel CLI creates starts protected, so vercel-setup's new project ends here as well: these steps come between the proof and the save of the method, in vercel-previews' step 2 or vercel-setup's step 5, before the test drive's pull request opens, and end with the preview opened. A lock of another host (Cloudflare Access, a Netlify password) is not part of this setup.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Get the secret in place** (the Preview method task) with impeccable_vercel_bypass, under the status line *"Getting past Vercel's login"*. The tool does the rest with the Vercel CLI signed in as the person: it finds the project's secret labelled "Impeccable reviews", or shows one screen asking to create one, or, when the CLI cannot, one screen taking a pasted one; opens the preview with it; and saves it for reviews. The secret never comes back to you. Done when it answered `status`:
|
|
8
|
+
- `saved`: the preview is proven (`proven: true`); `secret` says whether it was created, existing or typed, and `project` which Vercel project holds it. Step 2.
|
|
9
|
+
- `skipped` or `refused`: nothing was saved; `reason` says what stopped it. Step 3.
|
|
10
|
+
- `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" in `left`.
|
|
11
|
+
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' step 2 says. Done when the method is saved.
|
|
12
|
+
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 Vercel's login, with the tool's `reason`, and name the settings page where the person makes the secret (the report's bullet below); end the report's "Not part of this setup" line with "Vercel's Deployment Protection in front of the previews". Done when the report says so.
|
|
13
|
+
|
|
14
|
+
### Reference
|
|
15
|
+
|
|
16
|
+
- **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. Revoking or regenerating it stops reviews until this setup is run again.
|
|
17
|
+
- **The tool does the writes**: the secret as Impeccable's credential for the lock, and the setting that sends it in the `x-vercel-protection-bypass` header, 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.
|
|
18
|
+
- **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.
|
|
19
|
+
- **The secret is never yours**: it goes from the command's own process to Vercel and to Impeccable, and into no message, status line, file, command or report. `vercel project protection`, `vercel api` and `vercel curl` print it, and the fence refuses them.
|
|
20
|
+
- **The app's own sign-in** is another card: a preview opened past the lock proves nothing about it.
|
|
21
|
+
- **Status lines** belong to the Preview method task. Word Vercel's login as a login.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
Every run, once the settings are saved. The person merges the setup pull request and closes the test drive, so they must tell the two apart at a glance. The test drive exists to show a rendered review, so its pull request opens only once its preview is proven: **no preview, no test drive**.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **The setup commit**, when the repository needs a change to keep previews working: the workflow and any fix the build needed, nothing else. With the workflow current on the default branch and no fix, there is none. Check access as workflow-as-person says, branch `impeccable/setup` from `origin/<defaultBranch>`, and bring in the workflow and commit as that case says. Push it in step 7, after the test drive. Done when that case's steps are done.
|
|
8
|
+
2. **The test drive page**, always. The test drive shows the person a whole review: pages to capture, a finding for each measure, and a new page, which is what starts the review's visitor sessions. Branch `impeccable/test-drive` from the setup commit, so it carries the same workflow and its preview runs from its own head (from the default branch when there is no setup commit). Add one small, self-contained page in the repository's own framework, such as `/impeccable-test-drive`: a file where the routing finds pages, plus its styles, with a heading, a paragraph and a few elements in the app's own words (so the review judges it as one of the app's pages), and the flaws below. Link it from a page **a visitor opens without signing in**, the landing page when it is one, and keep the new page open without signing in too: no guard, on the server or in the browser, sends it to the sign-in. Done when the new page carries every flaw below with its comment, and the page you picked links to it.
|
|
9
|
+
3. **Prove both pages** with impeccable_verify_preview, for a preview Impeccable builds itself (ci-tunnel, static-site): the input that proved the preview, with both pages in `paths` for a server (a redirect to the sign-in fails there) or in `entries` for a static site. A host's preview (vercel-previews, vercel-setup, netlify-previews, netlify-setup) is proven on the host's own build in step 4 instead; its case says how the branch is built here first. Done when it answers `verified: true`, or the preview is a host's.
|
|
10
|
+
4. **Push the test drive and prove its preview**: commit, `git push -u origin impeccable/test-drive`, then the way its preview comes:
|
|
11
|
+
- **Vercel** (vercel-previews, vercel-setup) builds every push: impeccable_hosted_previews with `branch: "impeccable/test-drive"` waits up to five minutes for its deployment of the pushed head, under a status line with its timer. Prove the test drive's page on that address with impeccable_prove_preview (past Vercel's login as preview-protection says), and save the method as the case says. Then step 5.
|
|
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
|
+
- **Impeccable's own preview** (ci-tunnel, 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
|
+
|
|
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.
|
|
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
|
+
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
|
+
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".
|
|
19
|
+
|
|
20
|
+
### Reference
|
|
21
|
+
|
|
22
|
+
- **The flaws**: one for each measure the review reports, each in one obvious place with a comment beside it (*"Intentional, for Impeccable's test drive: link text under the 11px floor."*). They change how the page looks, never whether it builds.
|
|
23
|
+
- **A slop tell**: gradient text on the heading (`background-clip: text` over a gradient).
|
|
24
|
+
- **A declaration off the DESIGN.md tokens**, when the repository has a DESIGN.md: a hard-coded colour its palette lacks.
|
|
25
|
+
- **Functional text under the floor**: a link or a button label at 10px.
|
|
26
|
+
- **Low-contrast text**: text in a solid light grey, well under 4.5:1 against its background.
|
|
27
|
+
- **A layout transition**: `transition: width 0.3s`, naming the layout property.
|
|
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, and put "Mark the test drive #N ready once Netlify builds its preview" in `left`.
|
|
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
|
+
- **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.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
The app has its own sign-in: a form that takes a username or an email and a password and checks them itself. The state's `signin` item, when there is one, lists the roles the scan read (`rolesDetected`), the ones saved (`rolesSaved`) and the ones still missing (`rolesMissing`). Any other sign-in (OAuth, a magic link, single sign-on, a login URL) is not part of this setup: name it on the report's "Not part of this setup" line.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Find the roles** in the auth code (the login route, the user model, role checks and route guards) and the seed script (`prisma/seed.ts`, `db/seeds.rb`, fixtures). A role is a kind of user that sees different pages; an app without roles has one, `default`. Done when every role has a name, and you know whether the seed creates an account for it and under which username or email.
|
|
8
|
+
2. **Ask for the accounts**, back to back. Done when each role has a username and a password reference, or was skipped.
|
|
9
|
+
- **Every role saved already** (`rolesMissing` empty, `rolesSaved` not): ask nothing, not even the trust screen, and go to step 4.
|
|
10
|
+
- Otherwise first, once, the trust screen: `kind: "confirm"`, `title` *"This app has a sign-in"*, `hint` word for word *"Impeccable reviews the pages people see after signing in. It needs one test account per role, used only to sign in. Passwords are stored encrypted and never shown again."*, `prompt` *"Share test accounts for reviews?"*, `confirmLabel` *"Share test accounts"*, `skipLabel` *"Skip for now"*. A skip ends the account questions: ask for no role, and every role counts as skipped.
|
|
11
|
+
- Then one question per role: `kind: "account"`, `title` the role's label (*"Admin account"*), `prompt` *"Reviews sign in with this account as admin."*, and `prefill` the seed's username or email when it names one. It answers the username and a password reference, or a skip, which skips that role only.
|
|
12
|
+
- **Some roles saved, some missing** (neither list empty): the write replaces the saved list as a whole, and the saved passwords never reach this run, so ask for every role, the saved ones first. The first account's hint: *"Saved accounts are typed again too, as the list is replaced whole; skip to add only the missing ones on the setup page."* A skip ends the questions, and the roles not asked yet count as skipped.
|
|
13
|
+
3. **Save them** with one impeccable_write to `test-accounts`: every answered role in the body `writes.testAccounts` gives, each `password` the reference its question returned. When `rolesSaved` is not empty and the person skipped any role, write nothing: the saved accounts stay as they are. Done when the answer names the saved roles, or nothing was written.
|
|
14
|
+
4. **Report** the saved roles under "What was configured, and by whom", and the skipped ones under "What was skipped, and why". Each role still without a saved account goes into `left` ("Add the staff test account on the setup page"). Done when every role is in the report once.
|
|
15
|
+
|
|
16
|
+
### Reference
|
|
17
|
+
|
|
18
|
+
- impeccable_ask and impeccable_write are the terminal prompt the instructions' `signIn` describes: the person types each password hidden, and it goes straight to `test-accounts`.
|
|
19
|
+
- **A password is only the reference impeccable_ask returned**; impeccable_write refuses any other, so a password in the seed, a README or an env file stays where it is. When the seed sets one, name its file in the account question's hint and let the person type it.
|
|
20
|
+
- `loginPage` is the login form's path when the code names one, such as `/login`.
|
|
21
|
+
- A `ci-tunnel` preview runs the seed, so the seed's accounts exist there.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
The pages need no server from this repository: plain HTML, or a build that writes a complete static site. Pages that need SSR, API routes or a backend here follow ci-tunnel; an external API they call does not change that. A build that writes only JavaScript, CSS or types is a library, not a site.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Find the output**: the folder the build writes the pages to (framework config, scripts), or, with no build, the folder that holds the real `index.html`. Done when you can name that folder and the build command, or that there is none.
|
|
8
|
+
2. **Prove it** with impeccable_verify_preview in static mode. Done when it answers `verified: true` with the folder's `index.html` in `files`; a failure names its `stage`, which the report explains, and pull-requests' "No preview, no test drive" says how the run goes on without a proof.
|
|
9
|
+
3. **Save it** with impeccable_write to `preview`: `intendedMethod: "static-upload"`, `outputDir` and the commands you proved, clearing a saved command you did not prove. Done when the answer's `keys` show each key stored or unchanged.
|
|
10
|
+
|
|
11
|
+
### Reference
|
|
12
|
+
|
|
13
|
+
- **No build**: `steps` is empty and the output is the page's folder, `.` for the checkout's root. Invent no manifest, build, runtime or server for plain HTML.
|
|
14
|
+
- **A base path**: `<base>/index.html` must exist under the output folder; pass `basePath` to the check, and save it.
|
|
15
|
+
- **A single-page app**: the write takes no `spa` key, and the entry check proves no deep link; report a route the preview cannot serve.
|
|
16
|
+
- A verified result proves the entry files exist, nothing more: rendering, assets, deep links and external APIs go under "What is not verified".
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
Vercel deploys previews of the pull requests: impeccable_hosted_previews lists a successful Preview deployment that Vercel made (`vercel: true`) of a pull request, with its address. The checkout often shows it too (`vercel.json`, `.vercel/project.json`, a `vercel` dependency), but GitHub's record decides. Vercel builds and serves every preview, so these steps replace the playbook's step 3, and the repository needs no workflow and no setup pull request.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Prove the newest preview** with impeccable_prove_preview: the address of the newest successful Preview deployment of a pull request (or a page on it a visitor opens without signing in, when the app's own sign-in answers there), and words the page shows that the checkout holds, such as the app's name in its title. Done when it answers `proven: true`; when an address no longer answers, try the next newest, three at most.
|
|
8
|
+
2. **Save the method** with impeccable_write to `preview`: `{"intendedMethod": "preview-deploy"}`. The proof stands in for the person's answer, so ask nothing. Done when the answer's `keys` show `intendedMethod` stored or unchanged.
|
|
9
|
+
|
|
10
|
+
### Reference
|
|
11
|
+
|
|
12
|
+
- **Behind Vercel's login** (`gate: "vercel-protection"`): Deployment Protection stands in front of the previews. Get past it as preview-protection says, which proves the preview, and go on with step 2; when nothing got past it, that case says how the run ends.
|
|
13
|
+
- **Another app, or no answer from any of the three**: Vercel's previews do not show this app. Set the preview up as ci-tunnel or static-site says instead, and say under "What is not verified" what the Vercel address answered.
|
|
14
|
+
- **The pull requests**: no setup pull request; the test drive branches from the default branch, and Vercel builds its preview from the push, which pull-requests' step 4 waits for and proves the test drive's page on before the pull request opens. In place of pull-requests' step 3, build the test drive branch once with the install and build Vercel runs (`vercel.json`'s `installCommand` and `buildCommand`, else the package's), as plain commands (`pnpm install`, then `pnpm run build`) and not with impeccable_verify_preview, which is for a preview Impeccable starts itself, so a page that does not build is never pushed.
|
|
15
|
+
- The evidence is GitHub's: link, deploy and change nothing on Vercel, and save no address pattern, since Vercel reports each preview's address to GitHub itself.
|
|
16
|
+
- The report names the address that answered with this app under "What we found", and puts the test drive's own preview under "What is not verified".
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
### When this applies
|
|
2
|
+
|
|
3
|
+
The checkout is set up for Vercel (`vercel.json`, `.vercel/project.json`, a `vercel` dependency or script) and Vercel has never deployed it: impeccable_hosted_previews lists no deployment that Vercel made (`vercel: true`), and no `error` (an error says nothing either way). These steps replace the playbook's step 3. As in vercel-previews, the repository needs no workflow and no setup pull request.
|
|
4
|
+
|
|
5
|
+
### Steps
|
|
6
|
+
|
|
7
|
+
1. **Sign in**, before any other preview work, with impeccable_ask: `kind: "confirm"`, `id: "vercel-setup"`. The tool does the rest: it shows the sign-in screen only when the Vercel CLI is signed out (a person signed in already is asked nothing), runs the login in the browser and offers a retry. Done when it answered `confirmed`, with `team` the slug the project goes into.
|
|
8
|
+
- **`cli: "missing"`**: install the CLI with the person's package manager (`npm install -g vercel`, `pnpm add -g vercel`, `yarn global add vercel` or `bun add -g vercel`), then ask again: the screen shows no second time.
|
|
9
|
+
- **`teams` in the answer instead of `team`** (several teams, none current): ask once which owns the project, `kind: "single"`, `id: "vercel-team"`, `options` the teams as given (the tool words the rest). The pick is the team.
|
|
10
|
+
- **Neither `team` nor `teams`**: the account has no team, and `vercel link --yes` without `--team` links in the person's own scope.
|
|
11
|
+
- **Skip**: set the preview up as ci-tunnel 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
|
+
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** with impeccable_vercel_connect: it runs the connection on the linked project and asks the person for Vercel's GitHub app's access when the app cannot reach the repository. Done when it answered `connected: true`; a project connected by the link already is, and it says so.
|
|
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
|
+
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
|
+
|
|
17
|
+
### Reference
|
|
18
|
+
|
|
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 ci-tunnel 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
|
+
- **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.
|
|
21
|
+
- **The build failed** (`state` is `failure` or `error`): `vercel deploy --yes` builds this checkout on Vercel and prints why it failed, so the report can name the cause.
|
|
22
|
+
- **No deployment within five minutes**: the build had not finished, or Vercel builds no push here: the project's Git connection is the thing to check, and "Run the setup again once Vercel builds the repository" goes in `left`.
|
|
23
|
+
- **Behind Vercel's login** (`gate: "vercel-protection"`) with nothing getting past it: preview-protection says what the report names.
|
|
24
|
+
- **The first deployment** of a new project is called Production, whatever branch it builds: it is the test drive's preview, and the proof may open it. Vercel serves a Production deployment at the project's own address (`vercel project ls` prints it) without its login, so the report tells the person that the test drive's build is public there until the project's default branch deploys.
|
|
25
|
+
- **Behind Vercel's login** (`gate: "vercel-protection"`): a project the CLI creates starts with Vercel Authentication on, so expect it, and go on as the preview-protection case says.
|
|
26
|
+
- **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. 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".
|
|
27
|
+
- **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 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.
|