deeplink-parity 0.6.4 → 0.7.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 +24 -2
- package/dist/baseline.js +69 -0
- package/dist/cli.js +39 -4
- package/dist/report/console.js +4 -2
- package/dist/report/github.js +6 -2
- package/dist/routes/init.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,8 +59,8 @@ npx deeplink-parity ./my-app-ios ./my-app-android
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
Findings show up as annotations on the run. Add `sha256` to include the Android signing
|
|
62
|
-
key in the check
|
|
63
|
-
|
|
62
|
+
key in the check. Already have findings? Freeze them once with `npx deeplink-parity
|
|
63
|
+
baseline` and commit the file — the check then fails only on new ones.
|
|
64
64
|
|
|
65
65
|
Domains are checked with zero setup. To also compare the **screens behind the links**,
|
|
66
66
|
run `npx deeplink-parity init` once — see [Route parity](#route-parity).
|
|
@@ -74,6 +74,7 @@ run `npx deeplink-parity init` once — see [Route parity](#route-parity).
|
|
|
74
74
|
- [Usage](#usage)
|
|
75
75
|
- [GitHub Action](#github-action)
|
|
76
76
|
- [In CI](#in-ci)
|
|
77
|
+
- [Start with existing findings](#start-with-existing-findings)
|
|
77
78
|
- [Run it on a schedule](#run-it-on-a-schedule)
|
|
78
79
|
- [Validate before deploying](#validate-before-deploying)
|
|
79
80
|
- [Route parity](#route-parity)
|
|
@@ -135,10 +136,12 @@ No configuration file. Domains are discovered from your app, then the matching w
|
|
|
135
136
|
```bash
|
|
136
137
|
npx deeplink-parity [path...] [options] # check (default)
|
|
137
138
|
npx deeplink-parity init [path...] # interactive setup for route comparison
|
|
139
|
+
npx deeplink-parity baseline [path...] # freeze current findings; fail on new ones only
|
|
138
140
|
|
|
139
141
|
--sha256 <fingerprint> Android signing fingerprint to look for in assetlinks.json
|
|
140
142
|
--well-known <dir> Read well-known files from <dir>/<domain>/ instead of the network
|
|
141
143
|
--config <file> Route config (default: deeplink-parity.yml in cwd or a root)
|
|
144
|
+
--baseline <file> Baseline file (default: deeplink-parity-baseline.json in cwd or a root)
|
|
142
145
|
--print-routes Print the extracted route tables before the report
|
|
143
146
|
--json Machine-readable output on stdout
|
|
144
147
|
--output <file> Also write the JSON result to a file
|
|
@@ -186,6 +189,7 @@ Findings appear as annotations on the run, and counts are available to later ste
|
|
|
186
189
|
| `sha256` | — | Android signing fingerprint |
|
|
187
190
|
| `well-known` | — | Read from disk instead of the network |
|
|
188
191
|
| `config` | auto | Route config file (see [Route parity](#route-parity)) |
|
|
192
|
+
| `baseline` | auto | Baseline file (see [Start with existing findings](#start-with-existing-findings)) |
|
|
189
193
|
| `fail-on` | `error` | `never` to report without failing the step |
|
|
190
194
|
| `version` | pinned | npm version to run |
|
|
191
195
|
|
|
@@ -204,6 +208,24 @@ writes nothing but JSON to stdout; progress notes go to stderr. On GitHub Action
|
|
|
204
208
|
findings are also emitted as annotations, which appear on the run summary and against the
|
|
205
209
|
file when one is involved — set `--format github` to force it elsewhere.
|
|
206
210
|
|
|
211
|
+
### Start with existing findings
|
|
212
|
+
|
|
213
|
+
A tool that fails on day one gets turned off on day two. If the first run reports
|
|
214
|
+
problems you cannot fix immediately, freeze them:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
npx deeplink-parity baseline ./app-ios ./app-android
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
That writes `deeplink-parity-baseline.json` — commit it. From then on every check still
|
|
221
|
+
**reports** the known findings, marked `(baselined)`, but only a finding that is not in
|
|
222
|
+
the file fails the run. The file stores a one-line summary per finding, so a reviewer
|
|
223
|
+
can read what is being tolerated. When a baselined problem gets fixed, the check says
|
|
224
|
+
so; re-run `baseline` to tighten the file.
|
|
225
|
+
|
|
226
|
+
A known problem keeps its identity when the failure changes shape — a domain that
|
|
227
|
+
answered 404 yesterday and times out today is still one known problem, not a new alert.
|
|
228
|
+
|
|
207
229
|
### Run it on a schedule
|
|
208
230
|
|
|
209
231
|
Your deep-link configuration changes a few times a year. The things that break it do not live in your repo at all:
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { access, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
export const BASELINE_NAME = 'deeplink-parity-baseline.json';
|
|
4
|
+
/**
|
|
5
|
+
* Identity of a finding across runs. Domain problems key on rule + domain so a failure
|
|
6
|
+
* that changes shape (404 today, timeout tomorrow) stays one known problem; findings
|
|
7
|
+
* without a domain (route gaps) carry their identity in the message.
|
|
8
|
+
*/
|
|
9
|
+
export function baselineKey(f) {
|
|
10
|
+
return `${f.rule}|${f.domain ?? f.message}`;
|
|
11
|
+
}
|
|
12
|
+
/** Same lookup order as the route config: an explicit path wins, then cwd, then roots. */
|
|
13
|
+
export async function findBaseline(explicit, roots) {
|
|
14
|
+
if (explicit)
|
|
15
|
+
return resolve(explicit);
|
|
16
|
+
for (const dir of [process.cwd(), ...roots]) {
|
|
17
|
+
const candidate = join(dir, BASELINE_NAME);
|
|
18
|
+
try {
|
|
19
|
+
await access(candidate);
|
|
20
|
+
return candidate;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// keep looking
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export async function loadBaseline(path) {
|
|
29
|
+
const raw = JSON.parse(await readFile(path, 'utf8'));
|
|
30
|
+
if (!Array.isArray(raw?.entries)) {
|
|
31
|
+
throw new Error(`${path} is not a baseline file — expected an "entries" array`);
|
|
32
|
+
}
|
|
33
|
+
return new Set(raw.entries.map((e) => e.key));
|
|
34
|
+
}
|
|
35
|
+
/** Only findings that can fail a run belong in a baseline; info never fails. */
|
|
36
|
+
export async function writeBaseline(path, findings) {
|
|
37
|
+
const entries = new Map();
|
|
38
|
+
for (const f of findings) {
|
|
39
|
+
if (f.severity === 'info')
|
|
40
|
+
continue;
|
|
41
|
+
const key = baselineKey(f);
|
|
42
|
+
if (!entries.has(key)) {
|
|
43
|
+
entries.set(key, { key, summary: [f.domain, f.message].filter(Boolean).join(' — ') });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const sorted = [...entries.values()].sort((a, b) => a.key.localeCompare(b.key));
|
|
47
|
+
await writeFile(path, `${JSON.stringify({ entries: sorted }, null, 2)}\n`);
|
|
48
|
+
return sorted.length;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Marks known findings in place and reports which baseline entries are now stale.
|
|
52
|
+
* Exit-code policy lives in exitCodeFor: a baselined error no longer fails the run.
|
|
53
|
+
*/
|
|
54
|
+
export function applyBaseline(findings, baseline) {
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const f of findings) {
|
|
57
|
+
if (f.severity === 'info')
|
|
58
|
+
continue;
|
|
59
|
+
const key = baselineKey(f);
|
|
60
|
+
if (baseline.has(key)) {
|
|
61
|
+
f.baselined = true;
|
|
62
|
+
seen.add(key);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
matched: seen.size,
|
|
67
|
+
resolved: [...baseline].filter((k) => !seen.has(k)).sort(),
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
+
import { applyBaseline, BASELINE_NAME, findBaseline, loadBaseline, writeBaseline } from './baseline.js';
|
|
4
5
|
import { localSource, networkSource } from './fetch/wellKnown.js';
|
|
5
6
|
import { exitCodeFor, printReport } from './report/console.js';
|
|
6
7
|
import { printGithubAnnotations } from './report/github.js';
|
|
@@ -13,6 +14,7 @@ matches what is actually hosted, across iOS and Android.
|
|
|
13
14
|
Usage
|
|
14
15
|
deeplink-parity [path...] [options] check (default)
|
|
15
16
|
deeplink-parity init [path...] interactive setup for route comparison
|
|
17
|
+
deeplink-parity baseline [path...] freeze current findings; checks then fail on new ones only
|
|
16
18
|
|
|
17
19
|
Pass one path per checkout. iOS and Android usually live in separate
|
|
18
20
|
repositories, and comparing them is the point:
|
|
@@ -23,6 +25,7 @@ Options
|
|
|
23
25
|
--sha256 <fingerprint> Android signing fingerprint to look for in assetlinks.json
|
|
24
26
|
--well-known <dir> Read well-known files from <dir>/<domain>/ instead of the network
|
|
25
27
|
--config <file> Route config (default: deeplink-parity.yml in cwd or a root)
|
|
28
|
+
--baseline <file> Baseline file (default: deeplink-parity-baseline.json in cwd or a root)
|
|
26
29
|
--print-routes Print the extracted route tables before the report
|
|
27
30
|
--json Machine-readable output on stdout
|
|
28
31
|
--output <file> Also write the JSON result to a file
|
|
@@ -47,6 +50,7 @@ function parseArgs(argv) {
|
|
|
47
50
|
let wellKnown;
|
|
48
51
|
let output;
|
|
49
52
|
let config;
|
|
53
|
+
let baseline;
|
|
50
54
|
// Actions sets GITHUB_ACTIONS=true; annotate by default there
|
|
51
55
|
let format = process.env.GITHUB_ACTIONS === 'true' ? 'github' : 'console';
|
|
52
56
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -71,11 +75,13 @@ function parseArgs(argv) {
|
|
|
71
75
|
output = args[++i];
|
|
72
76
|
else if (arg === '--config')
|
|
73
77
|
config = args[++i];
|
|
78
|
+
else if (arg === '--baseline')
|
|
79
|
+
baseline = args[++i];
|
|
74
80
|
else if (!arg.startsWith('-'))
|
|
75
81
|
positional.push(arg);
|
|
76
82
|
}
|
|
77
|
-
const command = positional[0] === 'init'
|
|
78
|
-
const roots = (command === '
|
|
83
|
+
const command = positional[0] === 'init' || positional[0] === 'baseline' ? positional[0] : 'check';
|
|
84
|
+
const roots = (command === 'check' ? positional : positional.slice(1)).map((r) => resolve(r));
|
|
79
85
|
return {
|
|
80
86
|
command,
|
|
81
87
|
roots: roots.length ? roots : [resolve('.')],
|
|
@@ -88,6 +94,7 @@ function parseArgs(argv) {
|
|
|
88
94
|
wellKnown,
|
|
89
95
|
output,
|
|
90
96
|
config,
|
|
97
|
+
baseline,
|
|
91
98
|
format,
|
|
92
99
|
};
|
|
93
100
|
}
|
|
@@ -130,6 +137,9 @@ async function main() {
|
|
|
130
137
|
}
|
|
131
138
|
const configPath = await findConfig(opts.config, opts.roots);
|
|
132
139
|
const routes = configPath ? await loadRoutesConfig(configPath) : undefined;
|
|
140
|
+
// the baseline subcommand regenerates the file, so it must not also consume one
|
|
141
|
+
const baselinePath = opts.command === 'baseline' ? undefined : await findBaseline(opts.baseline, opts.roots);
|
|
142
|
+
const baseline = baselinePath ? await loadBaseline(baselinePath) : undefined;
|
|
133
143
|
const source = opts.wellKnown ? localSource(resolve(opts.wellKnown)) : networkSource();
|
|
134
144
|
const result = await run({
|
|
135
145
|
roots: opts.roots,
|
|
@@ -143,6 +153,26 @@ async function main() {
|
|
|
143
153
|
}
|
|
144
154
|
},
|
|
145
155
|
});
|
|
156
|
+
if (opts.command === 'baseline') {
|
|
157
|
+
const target = opts.baseline ? resolve(opts.baseline) : resolve(BASELINE_NAME);
|
|
158
|
+
const count = await writeBaseline(target, result.findings);
|
|
159
|
+
console.log(`Froze ${count} finding(s) into ${target}`);
|
|
160
|
+
console.log('Commit it. From now on the check fails only on findings that are not in this file.');
|
|
161
|
+
console.log('Re-run `deeplink-parity baseline` after fixing something, to tighten it.');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const applied = baseline ? applyBaseline(result.findings, baseline) : undefined;
|
|
165
|
+
if (applied) {
|
|
166
|
+
for (const key of applied.resolved) {
|
|
167
|
+
result.findings.push({
|
|
168
|
+
severity: 'info',
|
|
169
|
+
rule: 'baseline-resolved',
|
|
170
|
+
message: `A baselined finding no longer occurs: ${key}`,
|
|
171
|
+
detail: 'Re-run `deeplink-parity baseline` to tighten the baseline.',
|
|
172
|
+
source: baselinePath,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
146
176
|
const nothingFound = result.iosApps.length === 0 && result.androidApps.length === 0 && result.findings.length === 0;
|
|
147
177
|
if (nothingFound) {
|
|
148
178
|
console.error(`No app configuration declaring deep links was found in ${opts.roots.join(', ')}`);
|
|
@@ -162,6 +192,7 @@ async function main() {
|
|
|
162
192
|
const payload = {
|
|
163
193
|
version: pkg.version,
|
|
164
194
|
routeConfig: configPath,
|
|
195
|
+
baseline: baselinePath ? { path: baselinePath, matched: applied?.matched ?? 0 } : undefined,
|
|
165
196
|
ios: result.iosApps.map((a) => ({
|
|
166
197
|
entitlements: a.entitlementsPath,
|
|
167
198
|
teamId: a.teamId,
|
|
@@ -193,12 +224,16 @@ async function main() {
|
|
|
193
224
|
// machine-readable output in the same run.
|
|
194
225
|
if (opts.output)
|
|
195
226
|
await writeFile(opts.output, `${JSON.stringify(payload, null, 2)}\n`);
|
|
196
|
-
const
|
|
227
|
+
const baselineLine = baselinePath
|
|
228
|
+
? ` · baseline: ${applied?.matched ?? 0} known finding(s), failing on new only`
|
|
229
|
+
: '';
|
|
230
|
+
const routesLine = (configPath
|
|
197
231
|
? `routes: ${configPath}` +
|
|
198
232
|
(result.routes
|
|
199
233
|
? ` (iOS ${result.routes.ios?.paths.length ?? 0} · Android ${result.routes.android?.paths.length ?? 0})`
|
|
200
234
|
: '')
|
|
201
|
-
: 'routes: no config found — run "deeplink-parity init" to compare route tables'
|
|
235
|
+
: 'routes: no config found — run "deeplink-parity init" to compare route tables') +
|
|
236
|
+
baselineLine;
|
|
202
237
|
if (opts.json) {
|
|
203
238
|
console.log(JSON.stringify(payload, null, 2));
|
|
204
239
|
}
|
package/dist/report/console.js
CHANGED
|
@@ -28,7 +28,8 @@ export function printReport(findings, checkedDomains, meta) {
|
|
|
28
28
|
const head = paint(LABEL[severity], COLOR[severity]);
|
|
29
29
|
// Not every finding is tied to a domain — fall back to the rule id so the line is never blank
|
|
30
30
|
const subject = f.domain ? paint(f.domain, BOLD) : paint(f.rule, DIM);
|
|
31
|
-
|
|
31
|
+
const known = f.baselined ? ` ${paint('(baselined)', DIM)}` : '';
|
|
32
|
+
console.log(`${head} ${subject}${known}`);
|
|
32
33
|
console.log(` ${f.message}`);
|
|
33
34
|
if (f.detail)
|
|
34
35
|
console.log(` ${paint(f.detail, DIM)}`);
|
|
@@ -41,6 +42,7 @@ export function printReport(findings, checkedDomains, meta) {
|
|
|
41
42
|
console.log(counts.join(', '));
|
|
42
43
|
console.log();
|
|
43
44
|
}
|
|
45
|
+
/** A baselined error is a known problem being tracked — it must not fail the run. */
|
|
44
46
|
export function exitCodeFor(findings) {
|
|
45
|
-
return findings.some((f) => f.severity === 'error') ? 1 : 0;
|
|
47
|
+
return findings.some((f) => f.severity === 'error' && !f.baselined) ? 1 : 0;
|
|
46
48
|
}
|
package/dist/report/github.js
CHANGED
|
@@ -18,6 +18,8 @@ function escapeProperty(value) {
|
|
|
18
18
|
*/
|
|
19
19
|
export function printGithubAnnotations(findings) {
|
|
20
20
|
for (const f of findings) {
|
|
21
|
+
// a known finding stays visible but must not paint the run red
|
|
22
|
+
const level = f.baselined ? 'notice' : LEVEL[f.severity];
|
|
21
23
|
const props = [`title=${escapeProperty(`deeplink-parity ${f.rule}`)}`];
|
|
22
24
|
// Annotations anchor to a path inside the checkout. A source can also be a URL or an
|
|
23
25
|
// absolute path outside it — neither anchors to anything, so the annotation stays
|
|
@@ -25,7 +27,9 @@ export function printGithubAnnotations(findings) {
|
|
|
25
27
|
if (f.source && !/^https?:\/\//.test(f.source) && !isAbsolute(f.source)) {
|
|
26
28
|
props.push(`file=${escapeProperty(f.source)}`);
|
|
27
29
|
}
|
|
28
|
-
const body = [f.domain, f.message, f.detail]
|
|
29
|
-
|
|
30
|
+
const body = [f.baselined ? '(baselined)' : '', f.domain, f.message, f.detail]
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.join(' — ');
|
|
33
|
+
console.log(`::${level} ${props.join(',')}::${escapeData(body)}`);
|
|
30
34
|
}
|
|
31
35
|
}
|
package/dist/routes/init.js
CHANGED
|
@@ -83,6 +83,12 @@ async function pickRegex(rl, files, yes) {
|
|
|
83
83
|
* after printing its full content and asking. The scanned repositories are never touched.
|
|
84
84
|
*/
|
|
85
85
|
export async function runInit(roots, yes) {
|
|
86
|
+
// Without a terminal the prompts get EOF and the process would exit 0 having done
|
|
87
|
+
// nothing — a silent success, which is worse than a hang.
|
|
88
|
+
if (!yes && !process.stdin.isTTY) {
|
|
89
|
+
console.error('init is interactive — run it in a terminal, or pass --yes to accept the top suggestions.');
|
|
90
|
+
return 2;
|
|
91
|
+
}
|
|
86
92
|
console.log(`Scanning ${roots.length} root(s) for route tables…`);
|
|
87
93
|
const candidates = await findCandidates(roots);
|
|
88
94
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
package/package.json
CHANGED