@shipvo/cli 0.1.5 → 0.1.6
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 +3 -0
- package/dist/adopt.js +1 -2
- package/dist/agents.d.ts +15 -0
- package/dist/agents.js +68 -9
- package/dist/approvals.d.ts +9 -0
- package/dist/approvals.js +106 -0
- package/dist/audit.d.ts +1 -1
- package/dist/audit.js +28 -11
- package/dist/autohalt.d.ts +1 -1
- package/dist/autohalt.js +28 -16
- package/dist/bsdiff.d.ts +4 -0
- package/dist/bsdiff.js +465 -0
- package/dist/ci-comment.d.ts +38 -0
- package/dist/ci-comment.js +169 -0
- package/dist/ci-compare.d.ts +10 -0
- package/dist/ci-compare.js +25 -0
- package/dist/ci-shots.d.ts +9 -0
- package/dist/ci-shots.js +81 -0
- package/dist/ci.d.ts +8 -0
- package/dist/ci.js +64 -6
- package/dist/cli.js +23 -1
- package/dist/config.d.ts +13 -0
- package/dist/config.js +43 -3
- package/dist/doctor.js +63 -7
- package/dist/ensure-playwright.d.ts +4 -0
- package/dist/ensure-playwright.js +7 -2
- package/dist/explore-issues.d.ts +12 -0
- package/dist/explore-issues.js +50 -0
- package/dist/explore.d.ts +83 -7
- package/dist/explore.js +326 -77
- package/dist/flows.d.ts +31 -0
- package/dist/flows.js +264 -0
- package/dist/help.d.ts +2 -2
- package/dist/help.js +19 -10
- package/dist/hermes.d.ts +10 -0
- package/dist/hermes.js +50 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +41 -6
- package/dist/json-out.d.ts +53 -0
- package/dist/json-out.js +91 -0
- package/dist/keys-json.d.ts +1 -0
- package/dist/keys-json.js +71 -0
- package/dist/login.js +27 -11
- package/dist/lumi.d.ts +104 -0
- package/dist/lumi.js +364 -0
- package/dist/migrate.d.ts +27 -0
- package/dist/migrate.js +521 -0
- package/dist/preview.d.ts +1 -0
- package/dist/preview.js +4 -2
- package/dist/promote.d.ts +1 -1
- package/dist/promote.js +38 -16
- package/dist/publish.d.ts +14 -2
- package/dist/publish.js +126 -62
- package/dist/qr.d.ts +9 -0
- package/dist/qr.js +213 -0
- package/dist/report-md.d.ts +1 -0
- package/dist/report-md.js +1 -1
- package/dist/rotation.d.ts +57 -0
- package/dist/rotation.js +157 -0
- package/dist/run-bridge.js +356 -2
- package/dist/run-net.d.ts +39 -0
- package/dist/run-net.js +266 -0
- package/dist/run.d.ts +23 -0
- package/dist/run.js +101 -17
- package/dist/signing.js +13 -0
- package/dist/sim.js +7 -3
- package/dist/smoke.d.ts +28 -0
- package/dist/smoke.js +117 -0
- package/dist/spawn-bin.d.ts +11 -0
- package/dist/spawn-bin.js +21 -0
- package/dist/status.d.ts +26 -4
- package/dist/status.js +69 -5
- package/dist/templates/github-action/shipvo-check.yml +18 -0
- package/dist/test.d.ts +1 -1
- package/dist/test.js +81 -18
- package/package.json +4 -3
- package/skills/shipvo-cli/SKILL.md +31 -0
package/dist/bsdiff.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
// Binary patches in pure TypeScript, so `shipvo publish` needs no Python or native tool.
|
|
2
|
+
// Output is the standard BSDIFF40 format (bzip2 control, diff and extra blocks), which the Android and iOS runtimes already apply.
|
|
3
|
+
/** Larsson-Sadakane suffix sort, as used by bsdiff. Returns the suffix array of `s` (length n + 1, I[0] = n). */
|
|
4
|
+
function suffixSort(s) {
|
|
5
|
+
const n = s.length;
|
|
6
|
+
const I = new Int32Array(n + 1), V = new Int32Array(n + 1);
|
|
7
|
+
// Iterative, with an explicit stack: periodic input would overflow the call stack if this recursed.
|
|
8
|
+
// A task with mid >= 0 marks the equal group [start, mid) as sorted; it must run after the left part and before the right part.
|
|
9
|
+
const split = (start0, len0, h) => {
|
|
10
|
+
const stack = [start0, len0, -1];
|
|
11
|
+
while (stack.length) {
|
|
12
|
+
const mid = stack.pop(), len = stack.pop(), start = stack.pop();
|
|
13
|
+
if (mid >= 0) {
|
|
14
|
+
// `start` = jj, `len` = kk
|
|
15
|
+
for (let m = 0; m < len - start; m++)
|
|
16
|
+
V[I[start + m]] = len - 1;
|
|
17
|
+
if (start === len - 1)
|
|
18
|
+
I[start] = -1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (len < 16) {
|
|
22
|
+
for (let k = start, j; k < start + len; k += j) {
|
|
23
|
+
j = 1;
|
|
24
|
+
let x = V[I[k] + h];
|
|
25
|
+
for (let i = 1; k + i < start + len; i++) {
|
|
26
|
+
const v = V[I[k + i] + h];
|
|
27
|
+
if (v < x) {
|
|
28
|
+
x = v;
|
|
29
|
+
j = 0;
|
|
30
|
+
}
|
|
31
|
+
if (v === x) {
|
|
32
|
+
const t = I[k + j];
|
|
33
|
+
I[k + j] = I[k + i];
|
|
34
|
+
I[k + i] = t;
|
|
35
|
+
j++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (let i = 0; i < j; i++)
|
|
39
|
+
V[I[k + i]] = k + j - 1;
|
|
40
|
+
if (j === 1)
|
|
41
|
+
I[k] = -1;
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const x = V[I[start + (len >> 1)] + h];
|
|
46
|
+
let jj = 0, kk = 0;
|
|
47
|
+
for (let i = start; i < start + len; i++) {
|
|
48
|
+
const v = V[I[i] + h];
|
|
49
|
+
if (v < x)
|
|
50
|
+
jj++;
|
|
51
|
+
if (v === x)
|
|
52
|
+
kk++;
|
|
53
|
+
}
|
|
54
|
+
jj += start;
|
|
55
|
+
kk += jj;
|
|
56
|
+
let i = start, j = 0, k = 0;
|
|
57
|
+
while (i < jj) {
|
|
58
|
+
const v = V[I[i] + h];
|
|
59
|
+
if (v < x)
|
|
60
|
+
i++;
|
|
61
|
+
else if (v === x) {
|
|
62
|
+
const t = I[i];
|
|
63
|
+
I[i] = I[jj + j];
|
|
64
|
+
I[jj + j] = t;
|
|
65
|
+
j++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const t = I[i];
|
|
69
|
+
I[i] = I[kk + k];
|
|
70
|
+
I[kk + k] = t;
|
|
71
|
+
k++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
while (jj + j < kk) {
|
|
75
|
+
if (V[I[jj + j] + h] === x)
|
|
76
|
+
j++;
|
|
77
|
+
else {
|
|
78
|
+
const t = I[jj + j];
|
|
79
|
+
I[jj + j] = I[kk + k];
|
|
80
|
+
I[kk + k] = t;
|
|
81
|
+
k++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Pushed in reverse: left runs first, then the middle update, then right.
|
|
85
|
+
if (start + len > kk)
|
|
86
|
+
stack.push(kk, start + len - kk, -1);
|
|
87
|
+
stack.push(jj, kk, 0);
|
|
88
|
+
if (jj > start)
|
|
89
|
+
stack.push(start, jj - start, -1);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const buckets = new Int32Array(256);
|
|
93
|
+
for (let i = 0; i < n; i++)
|
|
94
|
+
buckets[s[i]]++;
|
|
95
|
+
for (let i = 1; i < 256; i++)
|
|
96
|
+
buckets[i] += buckets[i - 1];
|
|
97
|
+
for (let i = 255; i > 0; i--)
|
|
98
|
+
buckets[i] = buckets[i - 1];
|
|
99
|
+
buckets[0] = 0;
|
|
100
|
+
for (let i = 0; i < n; i++)
|
|
101
|
+
I[++buckets[s[i]]] = i;
|
|
102
|
+
I[0] = n;
|
|
103
|
+
for (let i = 0; i < n; i++)
|
|
104
|
+
V[i] = buckets[s[i]];
|
|
105
|
+
V[n] = 0;
|
|
106
|
+
for (let i = 1; i < 256; i++)
|
|
107
|
+
if (buckets[i] === buckets[i - 1] + 1)
|
|
108
|
+
I[buckets[i]] = -1;
|
|
109
|
+
I[0] = -1;
|
|
110
|
+
for (let h = 1; I[0] !== -(n + 1); h += h) {
|
|
111
|
+
let len = 0, i = 0;
|
|
112
|
+
while (i < n + 1) {
|
|
113
|
+
if (I[i] < 0) {
|
|
114
|
+
len -= I[i];
|
|
115
|
+
i -= I[i];
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
if (len)
|
|
119
|
+
I[i - len] = -len;
|
|
120
|
+
len = V[I[i]] + 1 - i;
|
|
121
|
+
split(i, len, h);
|
|
122
|
+
i += len;
|
|
123
|
+
len = 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (len)
|
|
127
|
+
I[i - len] = -len;
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < n + 1; i++)
|
|
130
|
+
I[V[i]] = i;
|
|
131
|
+
return I;
|
|
132
|
+
}
|
|
133
|
+
// ---- bzip2 writer (one Huffman table, blocks of up to 400 kB of input) ----
|
|
134
|
+
class Bits {
|
|
135
|
+
out = [];
|
|
136
|
+
acc = 0;
|
|
137
|
+
n = 0;
|
|
138
|
+
put(bits, value) {
|
|
139
|
+
while (bits > 16) {
|
|
140
|
+
bits -= 16;
|
|
141
|
+
this.put(16, Math.floor(value / 2 ** bits) & 0xffff);
|
|
142
|
+
value = value % 2 ** bits;
|
|
143
|
+
}
|
|
144
|
+
this.acc = this.acc * 2 ** bits + value;
|
|
145
|
+
this.n += bits;
|
|
146
|
+
while (this.n >= 8) {
|
|
147
|
+
this.n -= 8;
|
|
148
|
+
this.out.push(Math.floor(this.acc / 2 ** this.n) & 0xff);
|
|
149
|
+
this.acc = this.acc % 2 ** this.n;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finish() { if (this.n > 0)
|
|
153
|
+
this.put(8 - this.n, 0); return Uint8Array.from(this.out); }
|
|
154
|
+
}
|
|
155
|
+
const CRC = (() => {
|
|
156
|
+
const t = new Uint32Array(256);
|
|
157
|
+
for (let i = 0; i < 256; i++) {
|
|
158
|
+
let c = i << 24;
|
|
159
|
+
for (let k = 0; k < 8; k++)
|
|
160
|
+
c = c & 0x80000000 ? (c << 1) ^ 0x04c11db7 : c << 1;
|
|
161
|
+
t[i] = c >>> 0;
|
|
162
|
+
}
|
|
163
|
+
return t;
|
|
164
|
+
})();
|
|
165
|
+
/** Code lengths (at most `max`) for the given frequencies. */
|
|
166
|
+
function huffmanLengths(freq, max) {
|
|
167
|
+
let f = freq.map((x) => Math.max(1, x));
|
|
168
|
+
for (;;) {
|
|
169
|
+
const leaves = f.map((w, sym) => ({ w, sym })).sort((x, y) => x.w - y.w);
|
|
170
|
+
const merged = [];
|
|
171
|
+
let i1 = 0, i2 = 0;
|
|
172
|
+
const take = () => (i2 >= merged.length || (i1 < leaves.length && leaves[i1].w <= merged[i2].w) ? leaves[i1++] : merged[i2++]);
|
|
173
|
+
for (let left = leaves.length; left > 1; left--) {
|
|
174
|
+
const a = take(), b = take();
|
|
175
|
+
merged.push({ w: a.w + b.w, sym: -1, a, b });
|
|
176
|
+
}
|
|
177
|
+
const lens = new Array(f.length).fill(0);
|
|
178
|
+
const walk = (nd, d) => { if (nd.a && nd.b) {
|
|
179
|
+
walk(nd.a, d + 1);
|
|
180
|
+
walk(nd.b, d + 1);
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
lens[nd.sym] = Math.max(1, d); };
|
|
184
|
+
walk(merged[merged.length - 1], 0);
|
|
185
|
+
if (Math.max(...lens) <= max)
|
|
186
|
+
return lens;
|
|
187
|
+
f = f.map((x) => 1 + (x >> 1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function bzipBlock(bits, raw) {
|
|
191
|
+
// CRC of the original bytes.
|
|
192
|
+
let crc = 0xffffffff;
|
|
193
|
+
for (const b of raw)
|
|
194
|
+
crc = ((crc << 8) ^ CRC[((crc >>> 24) ^ b) & 0xff]) >>> 0;
|
|
195
|
+
crc = ~crc >>> 0;
|
|
196
|
+
// Initial run-length step: 4 equal bytes are followed by a count of further repeats.
|
|
197
|
+
const B = [];
|
|
198
|
+
for (let i = 0; i < raw.length;) {
|
|
199
|
+
let j = i;
|
|
200
|
+
while (j < raw.length && raw[j] === raw[i] && j - i < 255 + 4)
|
|
201
|
+
j++;
|
|
202
|
+
const run = j - i;
|
|
203
|
+
if (run >= 4) {
|
|
204
|
+
B.push(raw[i], raw[i], raw[i], raw[i], run - 4);
|
|
205
|
+
}
|
|
206
|
+
else
|
|
207
|
+
for (let k = 0; k < run; k++)
|
|
208
|
+
B.push(raw[i]);
|
|
209
|
+
i = j;
|
|
210
|
+
}
|
|
211
|
+
const n = B.length;
|
|
212
|
+
// Burrows-Wheeler transform through the suffix array of the doubled block.
|
|
213
|
+
const TT = new Uint8Array(2 * n);
|
|
214
|
+
TT.set(B);
|
|
215
|
+
TT.set(B, n);
|
|
216
|
+
const sa = suffixSort(TT);
|
|
217
|
+
const last = new Uint8Array(n);
|
|
218
|
+
let origPtr = 0, w = 0;
|
|
219
|
+
for (let k = 0; k <= 2 * n; k++) {
|
|
220
|
+
const p = sa[k];
|
|
221
|
+
if (p >= n)
|
|
222
|
+
continue;
|
|
223
|
+
if (p === 0)
|
|
224
|
+
origPtr = w;
|
|
225
|
+
last[w++] = B[(p + n - 1) % n];
|
|
226
|
+
}
|
|
227
|
+
// Symbol map, move-to-front and zero-run coding.
|
|
228
|
+
const used = new Array(256).fill(false);
|
|
229
|
+
for (const b of B)
|
|
230
|
+
used[b] = true;
|
|
231
|
+
const order = [];
|
|
232
|
+
for (let i = 0; i < 256; i++)
|
|
233
|
+
if (used[i])
|
|
234
|
+
order.push(i);
|
|
235
|
+
const nIn = order.length, EOB = nIn + 1, alpha = nIn + 2;
|
|
236
|
+
const mtf = order.slice();
|
|
237
|
+
const syms = [];
|
|
238
|
+
let zrun = 0;
|
|
239
|
+
const flush = () => { if (!zrun)
|
|
240
|
+
return; let z = zrun - 1; for (;;) {
|
|
241
|
+
syms.push(z & 1);
|
|
242
|
+
if (z < 2)
|
|
243
|
+
break;
|
|
244
|
+
z = (z - 2) >> 1;
|
|
245
|
+
} zrun = 0; };
|
|
246
|
+
for (let i = 0; i < n; i++) {
|
|
247
|
+
const b = last[i];
|
|
248
|
+
const j = mtf.indexOf(b);
|
|
249
|
+
if (j === 0) {
|
|
250
|
+
zrun++;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
flush();
|
|
254
|
+
mtf.splice(j, 1);
|
|
255
|
+
mtf.unshift(b);
|
|
256
|
+
syms.push(j + 1);
|
|
257
|
+
}
|
|
258
|
+
flush();
|
|
259
|
+
syms.push(EOB);
|
|
260
|
+
const freq = new Array(alpha).fill(0);
|
|
261
|
+
for (const s of syms)
|
|
262
|
+
freq[s]++;
|
|
263
|
+
const lens = huffmanLengths(freq, 17);
|
|
264
|
+
const codes = new Array(alpha).fill(0);
|
|
265
|
+
{
|
|
266
|
+
let code = 0;
|
|
267
|
+
for (let l = 1; l <= 17; l++) {
|
|
268
|
+
for (let s = 0; s < alpha; s++)
|
|
269
|
+
if (lens[s] === l)
|
|
270
|
+
codes[s] = code++;
|
|
271
|
+
code <<= 1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
bits.put(24, 0x314159);
|
|
275
|
+
bits.put(24, 0x265359);
|
|
276
|
+
bits.put(32, crc);
|
|
277
|
+
bits.put(1, 0);
|
|
278
|
+
bits.put(24, origPtr);
|
|
279
|
+
let top = 0;
|
|
280
|
+
const mapBits = [];
|
|
281
|
+
for (let g = 0; g < 16; g++) {
|
|
282
|
+
let any = false, m = 0;
|
|
283
|
+
for (let i = 0; i < 16; i++) {
|
|
284
|
+
m = (m << 1) | (used[g * 16 + i] ? 1 : 0);
|
|
285
|
+
any ||= used[g * 16 + i];
|
|
286
|
+
}
|
|
287
|
+
if (any)
|
|
288
|
+
top |= 1 << (15 - g);
|
|
289
|
+
mapBits.push(any ? m : -1);
|
|
290
|
+
}
|
|
291
|
+
bits.put(16, top);
|
|
292
|
+
for (const m of mapBits)
|
|
293
|
+
if (m >= 0)
|
|
294
|
+
bits.put(16, m);
|
|
295
|
+
const nSel = Math.ceil(syms.length / 50);
|
|
296
|
+
bits.put(3, 2);
|
|
297
|
+
bits.put(15, nSel);
|
|
298
|
+
for (let i = 0; i < nSel; i++)
|
|
299
|
+
bits.put(1, 0); // every group of 50 symbols uses table 0
|
|
300
|
+
for (let t = 0; t < 2; t++) { // the second table is the same; it is never selected
|
|
301
|
+
let cur = lens[0];
|
|
302
|
+
bits.put(5, cur);
|
|
303
|
+
for (let s = 0; s < alpha; s++) {
|
|
304
|
+
while (cur < lens[s]) {
|
|
305
|
+
bits.put(2, 2);
|
|
306
|
+
cur++;
|
|
307
|
+
}
|
|
308
|
+
while (cur > lens[s]) {
|
|
309
|
+
bits.put(2, 3);
|
|
310
|
+
cur--;
|
|
311
|
+
}
|
|
312
|
+
bits.put(1, 0);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const s of syms)
|
|
316
|
+
bits.put(lens[s], codes[s]);
|
|
317
|
+
return crc;
|
|
318
|
+
}
|
|
319
|
+
/** Compress to a standard .bz2 stream. */
|
|
320
|
+
export function bzip2(data) {
|
|
321
|
+
const bits = new Bits();
|
|
322
|
+
bits.put(8, 0x42);
|
|
323
|
+
bits.put(8, 0x5a);
|
|
324
|
+
bits.put(8, 0x68);
|
|
325
|
+
bits.put(8, 0x39);
|
|
326
|
+
let combined = 0;
|
|
327
|
+
for (let off = 0; off < data.length; off += 400_000) {
|
|
328
|
+
const crc = bzipBlock(bits, data.subarray(off, Math.min(off + 400_000, data.length)));
|
|
329
|
+
combined = ((((combined << 1) | (combined >>> 31)) ^ crc) >>> 0);
|
|
330
|
+
}
|
|
331
|
+
bits.put(24, 0x177245);
|
|
332
|
+
bits.put(24, 0x385090);
|
|
333
|
+
bits.put(32, combined);
|
|
334
|
+
return bits.finish();
|
|
335
|
+
}
|
|
336
|
+
// ---- bsdiff ----
|
|
337
|
+
function offtout(x, buf, o) {
|
|
338
|
+
let y = Math.abs(x);
|
|
339
|
+
for (let i = 0; i < 8; i++) {
|
|
340
|
+
buf[o + i] = y % 256;
|
|
341
|
+
y = Math.floor(y / 256);
|
|
342
|
+
}
|
|
343
|
+
if (x < 0)
|
|
344
|
+
buf[o + 7] |= 0x80;
|
|
345
|
+
}
|
|
346
|
+
/** A BSDIFF40 patch that turns `oldData` into `newData`. */
|
|
347
|
+
export function bsdiff(oldData, newData) {
|
|
348
|
+
const oldSize = oldData.length, newSize = newData.length;
|
|
349
|
+
const I = suffixSort(oldData);
|
|
350
|
+
const matchLen = (a, b) => { let i = 0; while (a + i < oldSize && b + i < newSize && oldData[a + i] === newData[b + i])
|
|
351
|
+
i++; return i; };
|
|
352
|
+
let pos = 0;
|
|
353
|
+
const search = (nOff, st, en) => {
|
|
354
|
+
while (en - st >= 2) {
|
|
355
|
+
const x = st + ((en - st) >> 1);
|
|
356
|
+
const a = I[x];
|
|
357
|
+
const lim = Math.min(oldSize - a, newSize - nOff);
|
|
358
|
+
let c = 0;
|
|
359
|
+
for (let i = 0; i < lim; i++) {
|
|
360
|
+
const d = oldData[a + i] - newData[nOff + i];
|
|
361
|
+
if (d) {
|
|
362
|
+
c = d;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Equal over the shared length: the shorter suffix sorts first, as in the suffix array (keeps long matches reachable on repetitive data).
|
|
367
|
+
if (c === 0 && oldSize - a < newSize - nOff)
|
|
368
|
+
c = -1;
|
|
369
|
+
if (c < 0)
|
|
370
|
+
st = x;
|
|
371
|
+
else
|
|
372
|
+
en = x;
|
|
373
|
+
}
|
|
374
|
+
const x = matchLen(I[st], nOff), y = matchLen(I[en], nOff);
|
|
375
|
+
if (x > y) {
|
|
376
|
+
pos = I[st];
|
|
377
|
+
return x;
|
|
378
|
+
}
|
|
379
|
+
pos = I[en];
|
|
380
|
+
return y;
|
|
381
|
+
};
|
|
382
|
+
const ctrl = [];
|
|
383
|
+
const diffB = [];
|
|
384
|
+
const extraB = [];
|
|
385
|
+
let scan = 0, len = 0, lastScan = 0, lastPos = 0, lastOffset = 0;
|
|
386
|
+
while (scan < newSize) {
|
|
387
|
+
let oldScore = 0;
|
|
388
|
+
for (let scsc = (scan += len); scan < newSize; scan++) {
|
|
389
|
+
len = search(scan, 0, oldSize);
|
|
390
|
+
for (; scsc < scan + len; scsc++)
|
|
391
|
+
if (scsc + lastOffset < oldSize && oldData[scsc + lastOffset] === newData[scsc])
|
|
392
|
+
oldScore++;
|
|
393
|
+
if ((len === oldScore && len !== 0) || len > oldScore + 8)
|
|
394
|
+
break;
|
|
395
|
+
if (scan + lastOffset < oldSize && oldData[scan + lastOffset] === newData[scan])
|
|
396
|
+
oldScore--;
|
|
397
|
+
}
|
|
398
|
+
if (len !== oldScore || scan === newSize) {
|
|
399
|
+
let s = 0, sf = 0, lenF = 0;
|
|
400
|
+
for (let i = 0; lastScan + i < scan && lastPos + i < oldSize;) {
|
|
401
|
+
if (oldData[lastPos + i] === newData[lastScan + i])
|
|
402
|
+
s++;
|
|
403
|
+
i++;
|
|
404
|
+
if (s * 2 - i > sf * 2 - lenF) {
|
|
405
|
+
sf = s;
|
|
406
|
+
lenF = i;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
let lenB = 0;
|
|
410
|
+
if (scan < newSize) {
|
|
411
|
+
s = 0;
|
|
412
|
+
let sb = 0;
|
|
413
|
+
for (let i = 1; scan >= lastScan + i && pos >= i; i++) {
|
|
414
|
+
if (oldData[pos - i] === newData[scan - i])
|
|
415
|
+
s++;
|
|
416
|
+
if (s * 2 - i > sb * 2 - lenB) {
|
|
417
|
+
sb = s;
|
|
418
|
+
lenB = i;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (lastScan + lenF > scan - lenB) {
|
|
423
|
+
const overlap = lastScan + lenF - (scan - lenB);
|
|
424
|
+
s = 0;
|
|
425
|
+
let ss = 0, lenS = 0;
|
|
426
|
+
for (let i = 0; i < overlap; i++) {
|
|
427
|
+
if (newData[lastScan + lenF - overlap + i] === oldData[lastPos + lenF - overlap + i])
|
|
428
|
+
s++;
|
|
429
|
+
if (newData[scan - lenB + i] === oldData[pos - lenB + i])
|
|
430
|
+
s--;
|
|
431
|
+
if (s > ss) {
|
|
432
|
+
ss = s;
|
|
433
|
+
lenS = i + 1;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
lenF += lenS - overlap;
|
|
437
|
+
lenB -= lenS;
|
|
438
|
+
}
|
|
439
|
+
for (let i = 0; i < lenF; i++)
|
|
440
|
+
diffB.push((newData[lastScan + i] - oldData[lastPos + i]) & 0xff);
|
|
441
|
+
const extraLen = scan - lenB - (lastScan + lenF);
|
|
442
|
+
for (let i = 0; i < extraLen; i++)
|
|
443
|
+
extraB.push(newData[lastScan + lenF + i]);
|
|
444
|
+
const c = new Uint8Array(24);
|
|
445
|
+
offtout(lenF, c, 0);
|
|
446
|
+
offtout(extraLen, c, 8);
|
|
447
|
+
offtout(pos - lenB - (lastPos + lenF), c, 16);
|
|
448
|
+
for (const b of c)
|
|
449
|
+
ctrl.push(b);
|
|
450
|
+
lastScan = scan - lenB;
|
|
451
|
+
lastPos = pos - lenB;
|
|
452
|
+
lastOffset = pos - scan;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const cz = bzip2(Uint8Array.from(ctrl)), dz = bzip2(Uint8Array.from(diffB)), ez = bzip2(Uint8Array.from(extraB));
|
|
456
|
+
const out = new Uint8Array(new ArrayBuffer(32 + cz.length + dz.length + ez.length));
|
|
457
|
+
out.set([0x42, 0x53, 0x44, 0x49, 0x46, 0x46, 0x34, 0x30]); // "BSDIFF40"
|
|
458
|
+
offtout(cz.length, out, 8);
|
|
459
|
+
offtout(dz.length, out, 16);
|
|
460
|
+
offtout(newSize, out, 24);
|
|
461
|
+
out.set(cz, 32);
|
|
462
|
+
out.set(dz, 32 + cz.length);
|
|
463
|
+
out.set(ez, 32 + cz.length + dz.length);
|
|
464
|
+
return out;
|
|
465
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TestReport } from "./report-md.js";
|
|
2
|
+
import type { Report as ExploreReport } from "./explore.js";
|
|
3
|
+
import { type Comparison } from "./ci-compare.js";
|
|
4
|
+
/** Hidden marker used to find our comment again. */
|
|
5
|
+
export declare const MARKER = "<!-- shipvo-check -->";
|
|
6
|
+
/** Pick screen names from the paths changed in a diff. Route files and screen files count; tests do not. */
|
|
7
|
+
export declare function changedScreens(files: string[]): string[];
|
|
8
|
+
/** Files changed since `base` (a commit sha). */
|
|
9
|
+
export declare function changedFiles(base: string): string[];
|
|
10
|
+
export type CommentInput = {
|
|
11
|
+
test?: TestReport | null;
|
|
12
|
+
explore?: ExploreReport | null;
|
|
13
|
+
changed?: string[];
|
|
14
|
+
screenshots?: string[];
|
|
15
|
+
runUrl?: string;
|
|
16
|
+
compare?: Comparison | null;
|
|
17
|
+
};
|
|
18
|
+
export declare function renderComment(i: CommentInput): string;
|
|
19
|
+
type Fetch = (url: string, init?: {
|
|
20
|
+
method?: string;
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
body?: string;
|
|
23
|
+
}) => Promise<{
|
|
24
|
+
ok: boolean;
|
|
25
|
+
status: number;
|
|
26
|
+
json(): Promise<any>;
|
|
27
|
+
}>;
|
|
28
|
+
/** Create the comment, or update the one that already has the marker. Returns "created" or "updated". */
|
|
29
|
+
export declare function upsertComment(o: {
|
|
30
|
+
repo: string;
|
|
31
|
+
pr: number;
|
|
32
|
+
token: string;
|
|
33
|
+
body: string;
|
|
34
|
+
fetch?: Fetch;
|
|
35
|
+
}): Promise<"created" | "updated">;
|
|
36
|
+
/** `shipvo ci comment [--changed-from <sha>] [--screenshots dir]`: render and post the comment. Needs GITHUB_TOKEN. */
|
|
37
|
+
export declare function commentCmd(argv: string[]): Promise<number>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The pull request comment for the Shipvo check: one comment, updated in place on each push.
|
|
2
|
+
// Everything runs in the user's own workflow. Nothing is sent to Shipvo.
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { diffReports } from "./ci-compare.js";
|
|
6
|
+
import { stepText } from "./report-md.js";
|
|
7
|
+
/** Hidden marker used to find our comment again. */
|
|
8
|
+
export const MARKER = "<!-- shipvo-check -->";
|
|
9
|
+
const MAX_SHOTS = 4;
|
|
10
|
+
const SCREEN_DIRS = /^(?:.*\/)?(?:app|screens|src\/screens|src\/app)\//;
|
|
11
|
+
const SCREEN_FILE = /\.(?:tsx|ts|jsx|js)$/;
|
|
12
|
+
/** Pick screen names from the paths changed in a diff. Route files and screen files count; tests do not. */
|
|
13
|
+
export function changedScreens(files) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const f of files) {
|
|
16
|
+
if (!SCREEN_DIRS.test(f) || !SCREEN_FILE.test(f) || /\.(?:test|spec)\./.test(f) || /(?:^|\/)__tests__\//.test(f))
|
|
17
|
+
continue;
|
|
18
|
+
const name = f.replace(SCREEN_DIRS, "").replace(SCREEN_FILE, "").replace(/\/index$/, "").replace(/^index$/, "home");
|
|
19
|
+
if (name && !name.startsWith("_") && !out.includes(name))
|
|
20
|
+
out.push(name);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
/** Files changed since `base` (a commit sha). */
|
|
25
|
+
export function changedFiles(base) {
|
|
26
|
+
return execFileSync("git", ["diff", "--name-only", `${base}...HEAD`], { encoding: "utf8" }).split("\n").filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
const cell = (s) => s.replace(/\s+/g, " ").replace(/\|/g, "\\|").trim();
|
|
29
|
+
const flowLine = (f) => {
|
|
30
|
+
const step = f.failedStep ? `first failing step: ${cell(stepText(f.failedStep))}` : "";
|
|
31
|
+
const msg = f.message ? cell(f.message).slice(0, 160) : "";
|
|
32
|
+
const why = [step, msg].filter(Boolean).join(". ");
|
|
33
|
+
return `- ${cell(f.name ?? "unnamed flow")}${why ? ` (${why})` : ""}`;
|
|
34
|
+
};
|
|
35
|
+
function compareSection(c) {
|
|
36
|
+
if (!c.regressions.length && !c.fixed.length && !c.stillFailing.length && !c.newFlows.some((f) => f.status === "failed"))
|
|
37
|
+
return ["### Compared with base", "", "No change in results.", ""];
|
|
38
|
+
const L = ["### Compared with base", ""];
|
|
39
|
+
const part = (title, flows) => { if (flows.length)
|
|
40
|
+
L.push(`**${title} (${flows.length})**`, "", ...flows.map(flowLine), ""); };
|
|
41
|
+
part("Regressions: passed on base, fail here", c.regressions);
|
|
42
|
+
part("Fixed", c.fixed);
|
|
43
|
+
part("Still failing", c.stillFailing);
|
|
44
|
+
part("New flows (not on base)", c.newFlows.filter((f) => f.status === "failed"));
|
|
45
|
+
return L;
|
|
46
|
+
}
|
|
47
|
+
export function renderComment(i) {
|
|
48
|
+
const flows = i.test?.flows ?? [];
|
|
49
|
+
const failed = flows.filter((f) => f.status === "failed");
|
|
50
|
+
const findings = i.explore?.findings ?? [];
|
|
51
|
+
const crashes = findings.filter((f) => f.kind === "error");
|
|
52
|
+
const deadEnds = findings.filter((f) => f.kind === "dead_end");
|
|
53
|
+
const other = findings.filter((f) => f.kind !== "error" && f.kind !== "dead_end");
|
|
54
|
+
const bad = failed.length > 0 || crashes.length > 0;
|
|
55
|
+
const L = [MARKER, `## Shipvo check: ${bad ? "failed" : "passed"}`, ""];
|
|
56
|
+
if (!i.test)
|
|
57
|
+
L.push("No test report was found. `shipvo test` may not have run.", "");
|
|
58
|
+
else if (!flows.length)
|
|
59
|
+
L.push("No flows were run.", "");
|
|
60
|
+
else {
|
|
61
|
+
L.push("| Flow | Result |", "| --- | --- |");
|
|
62
|
+
for (const f of flows)
|
|
63
|
+
L.push(`| ${cell(f.name ?? "unnamed flow")} | ${f.status === "failed" ? `❌ failed${f.message ? `: ${cell(f.message).slice(0, 160)}` : ""}` : "✅ passed"} |`);
|
|
64
|
+
L.push("");
|
|
65
|
+
}
|
|
66
|
+
if (i.compare)
|
|
67
|
+
L.push(...compareSection(i.compare));
|
|
68
|
+
if (i.explore) {
|
|
69
|
+
L.push("### Explore", "");
|
|
70
|
+
if (!findings.length)
|
|
71
|
+
L.push(`No findings in ${i.explore.steps} steps.`, "");
|
|
72
|
+
else {
|
|
73
|
+
L.push(`- Crashes or runtime errors: ${crashes.length}`, `- Dead ends: ${deadEnds.length}`, `- Accessibility and other problems: ${other.length}`, "");
|
|
74
|
+
for (const f of findings.slice(0, 10))
|
|
75
|
+
L.push(`- ${f.kind}: ${cell(f.message)} (screen ${cell(f.screen || "unknown")})`);
|
|
76
|
+
if (findings.length > 10)
|
|
77
|
+
L.push(`- and ${findings.length - 10} more in \`explore-report.md\``);
|
|
78
|
+
L.push("");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const shots = (i.screenshots ?? []).slice(0, MAX_SHOTS);
|
|
82
|
+
const changed = (i.changed ?? []).slice(0, MAX_SHOTS);
|
|
83
|
+
if (changed.length || shots.length) {
|
|
84
|
+
L.push("### Changed screens", "");
|
|
85
|
+
if (changed.length)
|
|
86
|
+
L.push(`From this diff: ${changed.map((c) => `\`${c}\``).join(", ")}`, "");
|
|
87
|
+
const where = i.runUrl ? `the \`shipvo-check\` artifact of [this run](${i.runUrl})` : "the `shipvo-check` artifact of this run";
|
|
88
|
+
if (shots.length)
|
|
89
|
+
L.push(`${shots.length} screenshot${shots.length === 1 ? "" : "s"} saved. Open ${where} and download the zip to see them.`, "");
|
|
90
|
+
else
|
|
91
|
+
L.push("No screenshots were captured for these screens.", "");
|
|
92
|
+
}
|
|
93
|
+
L.push("<sub>Built in your workflow. Nothing was sent to Shipvo.</sub>");
|
|
94
|
+
return L.join("\n");
|
|
95
|
+
}
|
|
96
|
+
/** Create the comment, or update the one that already has the marker. Returns "created" or "updated". */
|
|
97
|
+
export async function upsertComment(o) {
|
|
98
|
+
const f = o.fetch ?? fetch;
|
|
99
|
+
const base = `https://api.github.com/repos/${o.repo}`;
|
|
100
|
+
const headers = { authorization: `Bearer ${o.token}`, accept: "application/vnd.github+json", "content-type": "application/json" };
|
|
101
|
+
const call = async (url, method, body) => {
|
|
102
|
+
const r = await f(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
103
|
+
if (!r.ok)
|
|
104
|
+
throw new Error(`GitHub ${method} ${url} answered ${r.status}${r.status === 403 ? ". The token is read-only (for example on a pull request from a fork), so the comment was not posted" : ""}`);
|
|
105
|
+
return r.json();
|
|
106
|
+
};
|
|
107
|
+
for (let page = 1;; page++) {
|
|
108
|
+
const list = (await call(`${base}/issues/${o.pr}/comments?per_page=100&page=${page}`, "GET"));
|
|
109
|
+
const mine = list.find((c) => typeof c.body === "string" && c.body.startsWith(MARKER) && (!c.user?.type || c.user.type === "Bot"));
|
|
110
|
+
if (mine) {
|
|
111
|
+
await call(`${base}/issues/comments/${mine.id}`, "PATCH", { body: o.body });
|
|
112
|
+
return "updated";
|
|
113
|
+
}
|
|
114
|
+
if (list.length < 100)
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
await call(`${base}/issues/${o.pr}/comments`, "POST", { body: o.body });
|
|
118
|
+
return "created";
|
|
119
|
+
}
|
|
120
|
+
const readJson = (p) => { try {
|
|
121
|
+
return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
} };
|
|
126
|
+
/** `shipvo ci comment [--changed-from <sha>] [--screenshots dir]`: render and post the comment. Needs GITHUB_TOKEN. */
|
|
127
|
+
export async function commentCmd(argv) {
|
|
128
|
+
const val = (n) => { const i = argv.indexOf(n); return i >= 0 ? argv[i + 1] : undefined; };
|
|
129
|
+
const base = val("--changed-from");
|
|
130
|
+
const dir = val("--screenshots") ?? "shipvo-shots";
|
|
131
|
+
let changed = [];
|
|
132
|
+
if (base) {
|
|
133
|
+
try {
|
|
134
|
+
changed = changedScreens(changedFiles(base));
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`could not read the diff: ${e instanceof Error ? e.message : e}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const shots = existsSync(dir) ? readdirSync(dir).filter((n) => /\.png$/i.test(n)).sort().map((n) => `${dir}/${n}`) : [];
|
|
141
|
+
const env = process.env;
|
|
142
|
+
const test = readJson(val("--report") ?? "shipvo-report.json");
|
|
143
|
+
let compare = null;
|
|
144
|
+
const baseFile = val("--base-report");
|
|
145
|
+
if (baseFile) {
|
|
146
|
+
const baseReport = readJson(baseFile);
|
|
147
|
+
if (!baseReport)
|
|
148
|
+
console.error(`No comparison with base: could not read ${baseFile}.`);
|
|
149
|
+
else
|
|
150
|
+
compare = diffReports(baseReport, test);
|
|
151
|
+
}
|
|
152
|
+
const body = renderComment({ compare, test, explore: readJson("explore-report.json"), changed, screenshots: shots,
|
|
153
|
+
runUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : undefined });
|
|
154
|
+
const fail = argv.includes("--fail-on-regression") && !!compare?.regressions.length ? 1 : 0;
|
|
155
|
+
const pr = Number(val("--pr") ?? /refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "")?.[1]);
|
|
156
|
+
const missing = [!env.GITHUB_TOKEN && "GITHUB_TOKEN", !env.GITHUB_REPOSITORY && "GITHUB_REPOSITORY", !pr && "a pull request number"].filter(Boolean);
|
|
157
|
+
if (missing.length) {
|
|
158
|
+
console.log(body);
|
|
159
|
+
console.error(`Not posted: missing ${missing.join(", ")}.`);
|
|
160
|
+
return fail;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
console.log(`Comment ${await upsertComment({ repo: env.GITHUB_REPOSITORY, pr, token: env.GITHUB_TOKEN, body })}.`);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
console.error(`Not posted: ${e instanceof Error ? e.message : e}`);
|
|
167
|
+
}
|
|
168
|
+
return fail;
|
|
169
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TestReport } from "./report-md.js";
|
|
2
|
+
export type FlowResult = NonNullable<TestReport["flows"]>[number];
|
|
3
|
+
export type Comparison = {
|
|
4
|
+
regressions: FlowResult[];
|
|
5
|
+
fixed: FlowResult[];
|
|
6
|
+
stillFailing: FlowResult[];
|
|
7
|
+
newFlows: FlowResult[];
|
|
8
|
+
};
|
|
9
|
+
/** Returns null when either report is missing: there is nothing to compare. */
|
|
10
|
+
export declare function diffReports(base: TestReport | null, pr: TestReport | null): Comparison | null;
|