Skip to content

Commit df99423

Browse files
committed
feat: add installer doctor checks
1 parent b6bc614 commit df99423

3 files changed

Lines changed: 294 additions & 0 deletions

File tree

__tests__/e2e/install-script.e2e.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,56 @@ function writePrivateKeyFixture(
6161
return keyFile;
6262
}
6363

64+
function writeFakeAppDoctorBin(dir: string): string {
65+
const binDir = path.join(dir, 'fake-bin');
66+
fs.mkdirSync(binDir, { recursive: true });
67+
68+
const ghPath = path.join(binDir, 'gh');
69+
fs.writeFileSync(
70+
ghPath,
71+
[
72+
'#!/usr/bin/env bash',
73+
'set -euo pipefail',
74+
'if [ "${1:-}" = "auth" ] && [ "${2:-}" = "status" ]; then exit 0; fi',
75+
'if [ "${1:-}" = "api" ] && [ "${2:-}" = "/app" ]; then',
76+
' args="$*"',
77+
' case "$args" in',
78+
' *".id"*) printf "12345\\n"; exit 0 ;;',
79+
' *".client_id"*) printf "Iv1.doctor-client-id\\n"; exit 0 ;;',
80+
' *".slug"*) printf "review-router-doctor\\n"; exit 0 ;;',
81+
' *".permissions.actions"*) exit 0 ;;',
82+
' *".permissions.contents"*) printf "read\\n"; exit 0 ;;',
83+
' *".permissions.issues"*) printf "write\\n"; exit 0 ;;',
84+
' *".permissions.pull_requests"*) printf "write\\n"; exit 0 ;;',
85+
' esac',
86+
'fi',
87+
'printf "unexpected gh call: %s\\n" "$*" >&2',
88+
'exit 1',
89+
'',
90+
].join('\n')
91+
);
92+
fs.chmodSync(ghPath, 0o755);
93+
94+
const opensslPath = path.join(binDir, 'openssl');
95+
fs.writeFileSync(
96+
opensslPath,
97+
[
98+
'#!/usr/bin/env bash',
99+
'set -euo pipefail',
100+
'if [ "${1:-}" = "dgst" ]; then',
101+
' printf "fake-signature"',
102+
' exit 0',
103+
'fi',
104+
'printf "unexpected openssl call: %s\\n" "$*" >&2',
105+
'exit 1',
106+
'',
107+
].join('\n')
108+
);
109+
fs.chmodSync(opensslPath, 0o755);
110+
111+
return binDir;
112+
}
113+
64114
describe('review-router curl installer e2e', () => {
65115
it('generates github-actions bot workflow for OpenRouter auth without GitHub App setup', () => {
66116
const result = runInstaller({
@@ -72,6 +122,12 @@ describe('review-router curl installer e2e', () => {
72122

73123
expect(result.status).toBe(0);
74124
expect(fs.existsSync(result.workflowPath)).toBe(true);
125+
expect(result.stdout).toContain('ReviewRouter doctor');
126+
expect(result.stdout).toContain('Workflow files are present');
127+
expect(result.stdout).toContain(
128+
'Skipping remote secret/variable doctor in dry-run/local-only mode'
129+
);
130+
expect(result.stdout).toContain('Setup summary');
75131

76132
const workflow = workflowText(result.workflowPath);
77133
expect(workflow).toContain('name: ReviewRouter');
@@ -203,6 +259,7 @@ describe('review-router curl installer e2e', () => {
203259
});
204260

205261
expect(result.status).toBe(0);
262+
expect(result.stdout).toContain('ReviewRouter doctor');
206263
expect(result.stdout).toContain('Skipping CODEX_CONFIG_TOML by default');
207264
expect(result.stdout).not.toContain('gh secret set CODEX_CONFIG_TOML');
208265
const workflow = workflowText(result.workflowPath);
@@ -267,6 +324,9 @@ describe('review-router curl installer e2e', () => {
267324
});
268325

269326
expect(result.status).toBe(0);
327+
expect(result.stdout).toContain(
328+
'Skipping GitHub App doctor in dry-run/local-only mode'
329+
);
270330
expect(result.stdout).toContain('Saved GitHub App profile:');
271331
expect(result.stdout).toContain('Loaded GitHub App profile:');
272332
expect(result.stdout).toContain(
@@ -310,6 +370,37 @@ describe('review-router curl installer e2e', () => {
310370
);
311371
});
312372

373+
it('fails before writing workflows when GitHub App doctor detects missing Actions write permission', () => {
374+
const workdir = makeTempDir('airr-app-doctor-workdir-');
375+
const fakeBin = writeFakeAppDoctorBin(workdir);
376+
const privateKeyFile = writePrivateKeyFixture(workdir);
377+
378+
const result = runInstaller(
379+
{
380+
PATH: `${fakeBin}:${process.env.PATH ?? ''}`,
381+
REVIEW_ROUTER_LOCAL_ONLY: '0',
382+
REVIEW_ROUTER_SKIP_GH_CHECK: '0',
383+
REVIEW_ROUTER_IDENTITY: 'app',
384+
REVIEW_ROUTER_APP_SETUP: 'manual',
385+
REVIEW_ROUTER_AUTH: 'openrouter',
386+
REVIEW_ROUTER_PRESET: 'safe',
387+
REVIEW_ROUTER_OPENROUTER_API_KEY: 'or-test-key',
388+
REVIEW_ROUTER_APP_CLIENT_ID: 'Iv1.doctor-client-id',
389+
REVIEW_ROUTER_APP_ID: '12345',
390+
REVIEW_ROUTER_APP_SLUG: 'review-router-doctor',
391+
REVIEW_ROUTER_APP_PRIVATE_KEY_FILE: privateKeyFile,
392+
},
393+
workdir
394+
);
395+
396+
expect(result.status).toBe(1);
397+
expect(`${result.stdout}\n${result.stderr}`).toContain(
398+
'GitHub App is missing required permissions: actions:write (current: none)'
399+
);
400+
expect(fs.existsSync(result.workflowPath)).toBe(false);
401+
expect(fs.existsSync(result.interactionWorkflowPath)).toBe(false);
402+
});
403+
313404
it('keeps existing GitHub App credential env vars working without explicit app setup mode', () => {
314405
const appDir = makeTempDir('airr-compat-app-');
315406
const privateKeyFile = writePrivateKeyFixture(appDir);

docs/install.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,26 @@ curl -fsSL https://raw.githubusercontent.com/777genius/review-router/main/script
114114

115115
Secrets are never deleted automatically. Existing secrets and variables are overwritten only after confirmation.
116116

117+
## Installer Doctor
118+
119+
The installer runs a doctor check before opening the setup PR.
120+
121+
It verifies:
122+
123+
- workflow files were written
124+
- required secrets and variables exist in the selected repo/org scope
125+
- GitHub App profiles match their private key
126+
- GitHub App mode has required permissions: `Actions: write`, `Contents: read`, `Issues: write`, `Pull requests: write`
127+
- the selected App is installed on the target repository
128+
129+
If you intentionally need to bypass App validation for a temporary/manual setup, use:
130+
131+
```bash
132+
REVIEW_ROUTER_SKIP_APP_DOCTOR=1
133+
```
134+
135+
Do not use this for production rollout. Without `Actions: write`, `/rr skip` can record the signed ledger but cannot rerun the failed review check automatically.
136+
117137
## Secret scopes
118138

119139
| Scope | Where secrets live | Repository access | Best for |

0 commit comments

Comments
 (0)