@goliapkg/sentori-cli 1.5.0 → 1.6.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 +268 -26
- package/lib/artifacts.d.ts +7 -0
- package/lib/artifacts.d.ts.map +1 -1
- package/lib/artifacts.js +5 -1
- package/lib/artifacts.js.map +1 -1
- package/lib/compatibility.d.ts +6 -0
- package/lib/compatibility.d.ts.map +1 -0
- package/lib/compatibility.js +21 -0
- package/lib/compatibility.js.map +1 -0
- package/lib/index.js +97 -65
- package/lib/index.js.map +1 -1
- package/lib/srcbundle.d.ts +37 -2
- package/lib/srcbundle.d.ts.map +1 -1
- package/lib/srcbundle.js +181 -58
- package/lib/srcbundle.js.map +1 -1
- package/lib/upload.d.ts +25 -0
- package/lib/upload.d.ts.map +1 -1
- package/lib/upload.js +52 -1
- package/lib/upload.js.map +1 -1
- package/lib/workflow.d.ts +47 -0
- package/lib/workflow.d.ts.map +1 -0
- package/lib/workflow.js +289 -0
- package/lib/workflow.js.map +1 -0
- package/package.json +10 -6
- package/src/__tests__/artifacts.test.ts +1 -1
- package/src/__tests__/compatibility.test.ts +80 -0
- package/src/__tests__/expand-sourcemaps.test.ts +56 -0
- package/src/__tests__/rn-upload-hook.test.ts +35 -0
- package/src/__tests__/srcbundle.test.ts +66 -0
- package/src/__tests__/workflow.test.ts +102 -0
- package/src/artifacts.ts +12 -1
- package/src/compatibility.ts +21 -0
- package/src/index.ts +83 -66
- package/src/srcbundle.ts +150 -61
- package/src/upload.ts +51 -1
- package/src/workflow.ts +240 -0
- package/lib/source-bundle.d.ts +0 -28
- package/lib/source-bundle.d.ts.map +0 -1
- package/lib/source-bundle.js +0 -193
- package/lib/source-bundle.js.map +0 -1
package/lib/workflow.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
4
|
+
import { gunzipSync } from 'node:zlib';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { fetchArtifacts } from './artifacts.js';
|
|
7
|
+
import { checkCompatibility } from './compatibility.js';
|
|
8
|
+
import { dwarfBinariesIn } from './native-artifacts.js';
|
|
9
|
+
import { inspectSourceArchive, packSources } from './srcbundle.js';
|
|
10
|
+
import { expandSourcemapPaths, uploadArtifact } from './upload.js';
|
|
11
|
+
import { isStrict, lenientFail, stripStrict } from './lenient.js';
|
|
12
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const commonOptions = {
|
|
14
|
+
config: { type: 'string' }, release: { type: 'string' }, 'api-url': { type: 'string' },
|
|
15
|
+
'git-tag': { type: 'string' }, 'dry-run': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
16
|
+
};
|
|
17
|
+
export const WORKFLOW_HELP = `Release uploads:
|
|
18
|
+
sentori init --api-url <url> [--release <r>] [--sourcemap <path>] [--dsym <path>]
|
|
19
|
+
[--mapping <path>] [--sources <dir>] [--git-tag <tag>] [--config <file>]
|
|
20
|
+
sentori config validate [--config sentori.config.json] [--release <r>]
|
|
21
|
+
sentori doctor [--config sentori.config.json] [--api-url <url>]
|
|
22
|
+
sentori upload all [--config sentori.config.json] [--release <r>] [--git-tag <tag>]
|
|
23
|
+
[--dry-run] [--strict]
|
|
24
|
+
sentori verify [--config sentori.config.json] [--release <r>] [--git-tag <tag>]
|
|
25
|
+
sentori sources pack --release <r> --output sources.zip [--root .] [--git-tag <tag>]
|
|
26
|
+
[--exclude <path>] <source-dir-or-file...>
|
|
27
|
+
sentori upload srcbundle --release <r> [--root .] [--git-tag <tag>] <sources.zip|source-dir...>
|
|
28
|
+
|
|
29
|
+
Paths in config are relative to that config. Sources keep paths relative to sources.root.
|
|
30
|
+
Set the API-scope token in SENTORI_TOKEN (or the configured tokenEnv); init never stores it.
|
|
31
|
+
Release must match the SDK exactly, including its build number. SENTORI_RELEASE is supported.
|
|
32
|
+
Git mode reads the specified tag's commit, never the working tree or a guessed latest tag.
|
|
33
|
+
Verification checks every planned file's bytes and server validation; it does not claim event matching.
|
|
34
|
+
`;
|
|
35
|
+
function object(value) { return !!value && typeof value === 'object' && !Array.isArray(value); }
|
|
36
|
+
function strings(value, label) {
|
|
37
|
+
if (!Array.isArray(value) || value.some(x => typeof x !== 'string' || !x.trim()))
|
|
38
|
+
throw new Error(`${label} must be an array of paths`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
export function loadUploadConfig(path) {
|
|
42
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
43
|
+
if (!object(value) || value.version !== 1 || typeof value.apiUrl !== 'string' || !object(value.artifacts))
|
|
44
|
+
throw new Error('config requires version: 1, apiUrl, and artifacts; run sentori init');
|
|
45
|
+
if (Object.hasOwn(value, 'token'))
|
|
46
|
+
throw new Error('store the API token in the environment and set tokenEnv, not token in the config');
|
|
47
|
+
const allowed = new Set(['version', 'apiUrl', 'tokenEnv', 'release', 'artifacts', 'sources', 'sourceArchive']);
|
|
48
|
+
for (const key of Object.keys(value))
|
|
49
|
+
if (!allowed.has(key))
|
|
50
|
+
throw new Error(`unknown config field: ${key}`);
|
|
51
|
+
for (const [kind, paths] of Object.entries(value.artifacts)) {
|
|
52
|
+
if (!['sourcemap', 'dsym', 'mapping'].includes(kind))
|
|
53
|
+
throw new Error(`unknown artifact kind ${kind}; use sourcemap, dsym, or mapping`);
|
|
54
|
+
strings(paths, `artifacts.${kind}`);
|
|
55
|
+
}
|
|
56
|
+
if (value.release !== undefined && (typeof value.release !== 'string' || !value.release.trim()))
|
|
57
|
+
throw new Error('release must be a non-empty string');
|
|
58
|
+
if (value.sourceArchive !== undefined && typeof value.sourceArchive !== 'string')
|
|
59
|
+
throw new Error('sourceArchive must be a ZIP path');
|
|
60
|
+
if (value.sources !== undefined) {
|
|
61
|
+
if (!object(value.sources))
|
|
62
|
+
throw new Error('sources must declare include paths');
|
|
63
|
+
for (const key of Object.keys(value.sources))
|
|
64
|
+
if (!['root', 'include', 'exclude', 'gitTag'].includes(key))
|
|
65
|
+
throw new Error(`unknown sources field: ${key}`);
|
|
66
|
+
strings(value.sources.include, 'sources.include');
|
|
67
|
+
if (value.sources.exclude !== undefined)
|
|
68
|
+
strings(value.sources.exclude, 'sources.exclude');
|
|
69
|
+
for (const key of ['root', 'gitTag'])
|
|
70
|
+
if (value.sources[key] !== undefined && typeof value.sources[key] !== 'string')
|
|
71
|
+
throw new Error(`sources.${key} must be a string`);
|
|
72
|
+
}
|
|
73
|
+
if (value.sources && value.sourceArchive)
|
|
74
|
+
throw new Error('choose sources or sourceArchive, not both');
|
|
75
|
+
const tokenEnv = value.tokenEnv ?? 'SENTORI_TOKEN';
|
|
76
|
+
if (typeof tokenEnv !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(tokenEnv))
|
|
77
|
+
throw new Error('tokenEnv must name an environment variable');
|
|
78
|
+
return { ...value, tokenEnv };
|
|
79
|
+
}
|
|
80
|
+
function apiUrl(raw) {
|
|
81
|
+
const url = new URL(raw);
|
|
82
|
+
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
83
|
+
throw new Error('apiUrl must be an HTTP(S) instance URL without credentials, query, or fragment');
|
|
84
|
+
return url.toString().replace(/\/+$/, '');
|
|
85
|
+
}
|
|
86
|
+
function validateArtifact(kind, bytes, name) {
|
|
87
|
+
if (!bytes.length)
|
|
88
|
+
throw new Error(`empty artifact: ${name}`);
|
|
89
|
+
if (kind === 'sourcemap') {
|
|
90
|
+
const map = JSON.parse(bytes.toString('utf8'));
|
|
91
|
+
if (map?.version !== 3 || !(Array.isArray(map.sections) || Array.isArray(map.sources) && typeof map.mappings === 'string'))
|
|
92
|
+
throw new Error(`not a version 3 source map: ${name}`);
|
|
93
|
+
}
|
|
94
|
+
if (kind === 'proguard' && !/^\S.* -> \S+:\s*$/m.test(bytes.toString('utf8')))
|
|
95
|
+
throw new Error(`no ProGuard/R8 class mappings in ${name}`);
|
|
96
|
+
if (kind === 'dsym' && !['feedface', 'cefaedfe', 'feedfacf', 'cffaedfe', 'cafebabe', 'bebafeca', 'cafebabf', 'bfbafeca'].includes(bytes.subarray(0, 4).toString('hex')))
|
|
97
|
+
throw new Error(`not a Mach-O DWARF binary: ${name}`);
|
|
98
|
+
}
|
|
99
|
+
export function prepareUploadPlan(configPath, overrides = {}) {
|
|
100
|
+
const config = loadUploadConfig(configPath);
|
|
101
|
+
const base = dirname(resolve(configPath));
|
|
102
|
+
const release = overrides.release ?? process.env.SENTORI_RELEASE ?? config.release;
|
|
103
|
+
if (!release?.trim())
|
|
104
|
+
throw new Error('set --release, SENTORI_RELEASE, or config.release to the exact SDK release');
|
|
105
|
+
const plan = { release, apiUrl: apiUrl(overrides.apiUrl ?? process.env.SENTORI_API_URL ?? config.apiUrl), tokenEnv: config.tokenEnv, files: [] };
|
|
106
|
+
const add = (kind, path, name = basename(path)) => {
|
|
107
|
+
let bytes = readFileSync(path);
|
|
108
|
+
if (bytes[0] === 0x1f && bytes[1] === 0x8b) {
|
|
109
|
+
bytes = gunzipSync(bytes, { maxOutputLength: 512 * 1024 * 1024 });
|
|
110
|
+
name = name.replace(/\.gz$/, '');
|
|
111
|
+
}
|
|
112
|
+
validateArtifact(kind, bytes, name);
|
|
113
|
+
const digest = hash(bytes);
|
|
114
|
+
const collision = plan.files.find(file => file.kind === kind && file.name === name);
|
|
115
|
+
if (collision) {
|
|
116
|
+
if (collision.hash !== digest)
|
|
117
|
+
throw new Error(`two different ${kind} files would overwrite ${name}; choose distinct upload paths`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
plan.files.push({ kind, path, name, hash: digest, bytes: bytes.length });
|
|
121
|
+
};
|
|
122
|
+
for (const path of config.artifacts.sourcemap ?? []) {
|
|
123
|
+
const maps = expandSourcemapPaths(resolve(base, path));
|
|
124
|
+
if (!maps.length)
|
|
125
|
+
throw new Error(`no source maps under ${path}`);
|
|
126
|
+
for (const file of maps)
|
|
127
|
+
add('sourcemap', file.path, file.name);
|
|
128
|
+
}
|
|
129
|
+
for (const path of config.artifacts.dsym ?? []) {
|
|
130
|
+
const full = resolve(base, path);
|
|
131
|
+
const files = dwarfBinariesIn(full);
|
|
132
|
+
if (!files.length)
|
|
133
|
+
throw new Error(`no dSYM binaries under ${path}`);
|
|
134
|
+
for (const file of files)
|
|
135
|
+
add('dsym', file, lstatSync(full).isDirectory() ? `${basename(full)}-${basename(file)}` : basename(file));
|
|
136
|
+
}
|
|
137
|
+
for (const path of config.artifacts.mapping ?? [])
|
|
138
|
+
add('proguard', resolve(base, path));
|
|
139
|
+
if (config.sources) {
|
|
140
|
+
const packed = packSources(config.sources.include, release, {
|
|
141
|
+
root: resolve(base, config.sources.root ?? '.'), exclude: config.sources.exclude,
|
|
142
|
+
gitTag: overrides.gitTag ?? config.sources.gitTag,
|
|
143
|
+
});
|
|
144
|
+
const dir = resolve(base, '.sentori');
|
|
145
|
+
mkdirSync(dir, { recursive: true });
|
|
146
|
+
const archive = resolve(dir, `sources-${hash(packed.bytes)}.zip`);
|
|
147
|
+
writeFileSync(archive, packed.bytes);
|
|
148
|
+
add('srcbundle', archive, 'sources.zip');
|
|
149
|
+
plan.source = { files: packed.manifest.files.map(file => file.path), tag: packed.manifest.source.tag, commit: packed.manifest.source.commit };
|
|
150
|
+
}
|
|
151
|
+
else if (overrides.gitTag)
|
|
152
|
+
throw new Error('--git-tag requires sources in the config');
|
|
153
|
+
if (config.sourceArchive) {
|
|
154
|
+
const archive = resolve(base, config.sourceArchive);
|
|
155
|
+
const source = inspectSourceArchive(readFileSync(archive), release);
|
|
156
|
+
add('srcbundle', archive);
|
|
157
|
+
plan.source = { files: source.files, tag: source.manifest?.source.tag, commit: source.manifest?.source.commit };
|
|
158
|
+
}
|
|
159
|
+
if (!plan.files.length)
|
|
160
|
+
throw new Error('config selects no files; declare artifacts or sources');
|
|
161
|
+
return plan;
|
|
162
|
+
}
|
|
163
|
+
export async function verifyUploadPlan(plan, token) {
|
|
164
|
+
const result = await fetchArtifacts({ ...plan, token });
|
|
165
|
+
if (!result.known || result.release !== plan.release)
|
|
166
|
+
return [`release not found: ${plan.release}`];
|
|
167
|
+
const problems = [];
|
|
168
|
+
for (const file of plan.files) {
|
|
169
|
+
const matches = result.artifacts.filter(row => row.kind === file.kind && row.contentHash === file.hash
|
|
170
|
+
&& (file.kind === 'dsym' || row.name === file.name));
|
|
171
|
+
if (!matches.length)
|
|
172
|
+
problems.push(`missing exact bytes: ${file.kind} ${file.name}`);
|
|
173
|
+
else if (!matches.some(row => row.usable === true))
|
|
174
|
+
problems.push(`${matches.some(row => row.usable === false) ? 'unreadable' : 'not verified by server'}: ${file.kind} ${file.name}`);
|
|
175
|
+
}
|
|
176
|
+
return problems;
|
|
177
|
+
}
|
|
178
|
+
export async function runWorkflow(command, argv) {
|
|
179
|
+
const strict = isStrict(argv);
|
|
180
|
+
try {
|
|
181
|
+
const parsed = parseArgs({ args: stripStrict(argv), options: { ...commonOptions,
|
|
182
|
+
...(command === 'init' ? { sourcemap: { type: 'string', multiple: true }, dsym: { type: 'string', multiple: true }, mapping: { type: 'string', multiple: true }, sources: { type: 'string', multiple: true } } : {}),
|
|
183
|
+
} });
|
|
184
|
+
if (parsed.values.help) {
|
|
185
|
+
console.log(WORKFLOW_HELP);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
if (parsed.values['dry-run'] && command !== 'upload')
|
|
189
|
+
throw new Error('--dry-run is only supported by upload all; doctor and verify must contact the server');
|
|
190
|
+
const configPath = resolve(parsed.values.config ?? 'sentori.config.json');
|
|
191
|
+
if (command === 'init') {
|
|
192
|
+
const v = parsed.values;
|
|
193
|
+
const base = dirname(configPath);
|
|
194
|
+
const config = {
|
|
195
|
+
version: 1, apiUrl: apiUrl(parsed.values['api-url'] ?? process.env.SENTORI_API_URL ?? 'https://sentori.golia.jp'),
|
|
196
|
+
tokenEnv: 'SENTORI_TOKEN', ...(parsed.values.release ? { release: parsed.values.release } : {}),
|
|
197
|
+
artifacts: { sourcemap: v.sourcemap ?? [], dsym: v.dsym ?? [], mapping: v.mapping ?? [] },
|
|
198
|
+
};
|
|
199
|
+
const include = v.sources ?? ['src', 'Sources', 'ios', 'android/app/src'].filter(path => existsSync(resolve(base, path)));
|
|
200
|
+
if (include.length)
|
|
201
|
+
config.sources = { root: '.', include, exclude: [], ...(parsed.values['git-tag'] ? { gitTag: parsed.values['git-tag'] } : {}) };
|
|
202
|
+
else if (parsed.values['git-tag'])
|
|
203
|
+
throw new Error('--git-tag requires --sources paths');
|
|
204
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { flag: 'wx' });
|
|
205
|
+
console.log(`Created ${configPath}. Review source paths and build artifact paths. Keep .sentori/ out of version control.\nSet SENTORI_TOKEN and the exact SDK release, then run sentori config validate and sentori upload all.`);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (command === 'doctor') {
|
|
209
|
+
const config = loadUploadConfig(configPath);
|
|
210
|
+
const token = process.env[config.tokenEnv];
|
|
211
|
+
if (!token)
|
|
212
|
+
throw new Error(`set ${config.tokenEnv} to an API-scope token`);
|
|
213
|
+
const result = await fetchArtifacts({
|
|
214
|
+
apiUrl: apiUrl(parsed.values['api-url'] ?? process.env.SENTORI_API_URL ?? config.apiUrl), token,
|
|
215
|
+
release: parsed.values.release ?? process.env.SENTORI_RELEASE ?? config.release ?? '__sentori_compatibility_check__',
|
|
216
|
+
});
|
|
217
|
+
console.log(checkCompatibility(result, !!(config.sources || config.sourceArchive)));
|
|
218
|
+
console.log(`Node ${process.versions.node}; API token accepted. No files uploaded. Build artifacts and event/build matching were not checked.`);
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const plan = prepareUploadPlan(configPath, { release: parsed.values.release, apiUrl: parsed.values['api-url'], gitTag: parsed.values['git-tag'] });
|
|
222
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
223
|
+
if (command === 'validate' || parsed.values['dry-run']) {
|
|
224
|
+
console.log('Local files checked; no upload or server verification performed.');
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
const token = process.env[plan.tokenEnv];
|
|
228
|
+
if (!token)
|
|
229
|
+
throw new Error(`set ${plan.tokenEnv} to an API-scope token`);
|
|
230
|
+
// Check credentials before the first write. An unknown release is allowed.
|
|
231
|
+
console.log(checkCompatibility(await fetchArtifacts({ ...plan, token }), plan.files.some(file => file.kind === 'srcbundle')));
|
|
232
|
+
const failures = [];
|
|
233
|
+
if (command === 'upload') {
|
|
234
|
+
for (const file of plan.files) {
|
|
235
|
+
try {
|
|
236
|
+
// Prevent edits between planning and upload from invalidating the receipt.
|
|
237
|
+
let body = readFileSync(file.path);
|
|
238
|
+
if (body[0] === 0x1f && body[1] === 0x8b)
|
|
239
|
+
body = gunzipSync(body, { maxOutputLength: 512 * 1024 * 1024 });
|
|
240
|
+
if (hash(body) !== file.hash)
|
|
241
|
+
throw new Error('file changed after local validation');
|
|
242
|
+
const verdict = await uploadArtifact({ ...plan, token, ...file });
|
|
243
|
+
if (verdict.usable !== true)
|
|
244
|
+
failures.push(`${file.name}: ${verdict.usable === false ? 'unreadable' : 'not verified by server'}`);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
failures.push(`${file.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
failures.push(...await verifyUploadPlan(plan, token));
|
|
252
|
+
if (failures.length)
|
|
253
|
+
throw new Error(failures.join('\n'));
|
|
254
|
+
console.log(`Verified ${plan.files.length} exact file(s) for ${plan.release}: stored bytes match and format checks passed. Event/build matching is not asserted.`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
+
if (command === 'upload')
|
|
260
|
+
return lenientFail(strict, { failure: message, impact: 'some planned debug files are missing or unverified', retry: 'sentori upload all --config sentori.config.json; sentori verify --config sentori.config.json' });
|
|
261
|
+
console.error(`[sentori] ${message}`);
|
|
262
|
+
return 1;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export async function runSourcesPack(argv) {
|
|
266
|
+
try {
|
|
267
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: {
|
|
268
|
+
release: { type: 'string' }, root: { type: 'string' }, output: { type: 'string' },
|
|
269
|
+
'git-tag': { type: 'string' }, exclude: { type: 'string', multiple: true }, help: { type: 'boolean', short: 'h' },
|
|
270
|
+
} });
|
|
271
|
+
if (parsed.values.help) {
|
|
272
|
+
console.log(WORKFLOW_HELP);
|
|
273
|
+
return 0;
|
|
274
|
+
}
|
|
275
|
+
const release = parsed.values.release ?? process.env.SENTORI_RELEASE ?? '';
|
|
276
|
+
if (!parsed.values.output)
|
|
277
|
+
throw new Error('--output <sources.zip> is required');
|
|
278
|
+
const options = { root: parsed.values.root, gitTag: parsed.values['git-tag'], exclude: parsed.values.exclude };
|
|
279
|
+
const packed = packSources(parsed.positionals, release, options);
|
|
280
|
+
writeFileSync(parsed.values.output, packed.bytes, { flag: 'wx' });
|
|
281
|
+
console.log(JSON.stringify({ output: parsed.values.output, ...packed.manifest, stats: packed.stats }, null, 2));
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error(`[sentori] ${error instanceof Error ? error.message : String(error)}`);
|
|
286
|
+
return 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=workflow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.js","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goliapkg/sentori-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Sentori CLI \u2014 upload
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Sentori CLI \u2014 configure, upload and verify release debug files and source archives.",
|
|
5
5
|
"license": "Apache-2.0 OR MIT",
|
|
6
6
|
"author": "GOLIA K.K. <takagi@golia.jp> (https://golia.jp)",
|
|
7
7
|
"homepage": "https://sentori.golia.jp",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/goliajp/sentori.git",
|
|
10
|
+
"url": "git+https://github.com/goliajp/sentori-selfhosted.git",
|
|
11
11
|
"directory": "sdk/cli"
|
|
12
12
|
},
|
|
13
13
|
"bugs": {
|
|
14
|
-
"url": "https://github.com/goliajp/sentori/issues"
|
|
14
|
+
"url": "https://github.com/goliajp/sentori-selfhosted/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"sentori",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"type": "module",
|
|
23
23
|
"bin": {
|
|
24
|
+
"sentori": "./lib/index.js",
|
|
24
25
|
"sentori-cli": "./lib/index.js"
|
|
25
26
|
},
|
|
26
27
|
"main": "./lib/index.js",
|
|
@@ -31,10 +32,10 @@
|
|
|
31
32
|
"README.md"
|
|
32
33
|
],
|
|
33
34
|
"engines": {
|
|
34
|
-
"node": ">=18"
|
|
35
|
+
"node": ">=18.17"
|
|
35
36
|
},
|
|
36
37
|
"scripts": {
|
|
37
|
-
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"build": "rm -rf lib && tsc -p tsconfig.json",
|
|
38
39
|
"typecheck": "tsc --noEmit",
|
|
39
40
|
"test": "bun test",
|
|
40
41
|
"prepack": "bun run build"
|
|
@@ -46,5 +47,8 @@
|
|
|
46
47
|
},
|
|
47
48
|
"publishConfig": {
|
|
48
49
|
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"fflate": "^0.8.2"
|
|
49
53
|
}
|
|
50
54
|
}
|
|
@@ -65,7 +65,7 @@ describe('artifacts read', () => {
|
|
|
65
65
|
|
|
66
66
|
test('the release name is escaped — @ and + are ordinary in one', async () => {
|
|
67
67
|
seen.length = 0
|
|
68
|
-
const restore = serve(200, full)
|
|
68
|
+
const restore = serve(200, { ...full, release: 'focus-ai-app@5.4.26080601+382' })
|
|
69
69
|
try {
|
|
70
70
|
await fetchArtifacts({
|
|
71
71
|
apiUrl: 'https://x.test/',
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { checkCompatibility, CLI_VERSION } from '../compatibility.js'
|
|
6
|
+
import { fetchArtifacts, type ArtifactsResponse } from '../artifacts.js'
|
|
7
|
+
import { runWorkflow } from '../workflow.js'
|
|
8
|
+
|
|
9
|
+
const legacy: ArtifactsResponse = { release: 'app@1+7', known: false, artifacts: [], kinds: {}, missing: [] }
|
|
10
|
+
const modern = { ...legacy, compatibility: { serverVersion: '5.1.12', artifactProtocol: 1, sourceBundleFormats: ['zip', 'json'], verification: ['sha256', 'format'] } }
|
|
11
|
+
let root: string
|
|
12
|
+
const originalFetch = globalThis.fetch
|
|
13
|
+
const oldToken = process.env.SENTORI_TEST_UPLOAD_TOKEN
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
root = mkdtempSync(join(tmpdir(), 'sentori-compatibility-'))
|
|
16
|
+
process.env.SENTORI_TEST_UPLOAD_TOKEN = 'api-test'
|
|
17
|
+
})
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
globalThis.fetch = originalFetch
|
|
20
|
+
rmSync(root, { recursive: true, force: true })
|
|
21
|
+
if (oldToken === undefined) delete process.env.SENTORI_TEST_UPLOAD_TOKEN
|
|
22
|
+
else process.env.SENTORI_TEST_UPLOAD_TOKEN = oldToken
|
|
23
|
+
})
|
|
24
|
+
function config(sources = true): string {
|
|
25
|
+
const path = join(root, 'sentori.config.json')
|
|
26
|
+
writeFileSync(path, JSON.stringify({ version: 1, apiUrl: 'https://upload.test', tokenEnv: 'SENTORI_TEST_UPLOAD_TOKEN', release: legacy.release,
|
|
27
|
+
artifacts: sources ? {} : { mapping: ['mapping.txt'] }, ...(sources ? { sources: { include: ['src'] } } : {}) }))
|
|
28
|
+
return path
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('CLI/server compatibility', () => {
|
|
32
|
+
test('installed package determines --version, independently of server and app releases', () => {
|
|
33
|
+
expect(CLI_VERSION).toBe(JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version)
|
|
34
|
+
expect(checkCompatibility(modern, true)).toContain('server 5.1.12')
|
|
35
|
+
expect(checkCompatibility(legacy, false)).toContain('Legacy artifact API')
|
|
36
|
+
expect(() => checkCompatibility(legacy, true)).toThrow('does not advertise source ZIP')
|
|
37
|
+
expect(() => checkCompatibility({ ...modern, compatibility: { ...modern.compatibility, artifactProtocol: 2 } }, false)).toThrow('Unsupported server artifact protocol')
|
|
38
|
+
expect(() => checkCompatibility({ ...modern, compatibility: { ...modern.compatibility, sourceBundleFormats: ['json'] } }, true)).toThrow('does not support source ZIPs')
|
|
39
|
+
expect(() => checkCompatibility({ ...modern, compatibility: { ...modern.compatibility, verification: ['sha256'] } }, false)).toThrow('verification capabilities')
|
|
40
|
+
})
|
|
41
|
+
test('doctor works before build files exist and never sends a write request', async () => {
|
|
42
|
+
let requests = 0
|
|
43
|
+
globalThis.fetch = (async (_url, init) => {
|
|
44
|
+
expect(init?.method ?? 'GET').toBe('GET'); requests++
|
|
45
|
+
return Response.json(modern)
|
|
46
|
+
}) as typeof fetch
|
|
47
|
+
const path = config()
|
|
48
|
+
expect(await runWorkflow('doctor', ['--config', path])).toBe(0)
|
|
49
|
+
expect(requests).toBe(1)
|
|
50
|
+
expect(await runWorkflow('doctor', ['--config', path, '--dry-run'])).toBe(1)
|
|
51
|
+
expect(await runWorkflow('verify', ['--config', path, '--dry-run'])).toBe(1)
|
|
52
|
+
expect(requests).toBe(1)
|
|
53
|
+
})
|
|
54
|
+
test('old servers reject ZIP plans before uploading even the other artifact kinds', async () => {
|
|
55
|
+
mkdirSync(join(root, 'src')); writeFileSync(join(root, 'src/main.ts'), 'export const x = 1')
|
|
56
|
+
writeFileSync(join(root, 'mapping.txt'), 'A -> a:\n')
|
|
57
|
+
const path = config()
|
|
58
|
+
const value = JSON.parse(readFileSync(path, 'utf8')); value.artifacts.mapping = ['mapping.txt']; writeFileSync(path, JSON.stringify(value))
|
|
59
|
+
const methods: string[] = []
|
|
60
|
+
globalThis.fetch = (async (_url, init) => { methods.push(init?.method ?? 'GET'); return Response.json(legacy) }) as typeof fetch
|
|
61
|
+
expect(await runWorkflow('doctor', ['--config', path])).toBe(1)
|
|
62
|
+
expect(await runWorkflow('upload', ['--config', path, '--strict'])).toBe(1)
|
|
63
|
+
expect(await runWorkflow('upload', ['--config', path])).toBe(0)
|
|
64
|
+
expect(methods).toEqual(['GET', 'GET', 'GET'])
|
|
65
|
+
})
|
|
66
|
+
test('a legacy server remains usable for builds without source ZIPs', async () => {
|
|
67
|
+
const path = config(false)
|
|
68
|
+
globalThis.fetch = (async () => Response.json(legacy)) as typeof fetch
|
|
69
|
+
expect(await runWorkflow('doctor', ['--config', path])).toBe(0)
|
|
70
|
+
})
|
|
71
|
+
test('wrong inventory shapes and unauthorised tokens never become compatibility success', async () => {
|
|
72
|
+
const opts = { apiUrl: 'https://upload.test', token: 't', release: legacy.release }
|
|
73
|
+
for (const body of [{}, { ...legacy, release: 'another-app' }, { ...legacy, artifacts: null }]) {
|
|
74
|
+
globalThis.fetch = (async () => Response.json(body)) as typeof fetch
|
|
75
|
+
await expect(fetchArtifacts(opts)).rejects.toThrow('Unsupported artifact inventory response')
|
|
76
|
+
}
|
|
77
|
+
globalThis.fetch = (async () => Response.json({ error: 'admin_token_required' }, { status: 403 })) as typeof fetch
|
|
78
|
+
expect(await runWorkflow('doctor', ['--config', config()])).toBe(1)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// A directory of maps keeps its shape on the way to the server.
|
|
2
|
+
//
|
|
3
|
+
// The server matches a frame to a map by how many trailing path
|
|
4
|
+
// segments they share, because a mini program names every page's entry
|
|
5
|
+
// `index.js` and a web build names every chunk after its route. The
|
|
6
|
+
// CLI sent only the basename, so that matching never saw more than a
|
|
7
|
+
// filename however carefully it was written — and passing a directory,
|
|
8
|
+
// which the web getting-started page tells people to do, threw EISDIR.
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from 'bun:test'
|
|
15
|
+
|
|
16
|
+
import { expandSourcemapPaths } from '../upload.js'
|
|
17
|
+
|
|
18
|
+
const tree = (): string => {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), 'sentori-maps-'))
|
|
20
|
+
mkdirSync(join(root, 'pages/cart'), { recursive: true })
|
|
21
|
+
mkdirSync(join(root, 'pages/home'), { recursive: true })
|
|
22
|
+
writeFileSync(join(root, 'pages/cart/index.js.map'), '{}')
|
|
23
|
+
writeFileSync(join(root, 'pages/home/index.js.map'), '{}')
|
|
24
|
+
writeFileSync(join(root, 'pages/cart/index.js'), '//')
|
|
25
|
+
writeFileSync(join(root, 'main.js.map'), '{}')
|
|
26
|
+
return root
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('expandSourcemapPaths', () => {
|
|
30
|
+
it('keeps the path under the directory, so two index.js.map stay distinct', () => {
|
|
31
|
+
const names = expandSourcemapPaths(tree())
|
|
32
|
+
.map((t) => t.name)
|
|
33
|
+
.sort()
|
|
34
|
+
expect(names).toEqual(['main.js.map', 'pages/cart/index.js.map', 'pages/home/index.js.map'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('takes only maps, not the bundles beside them', () => {
|
|
38
|
+
// A bundle uploaded as a sourcemap is stored, unparseable, and
|
|
39
|
+
// lights the release row amber — which happened in production for
|
|
40
|
+
// months on a real release.
|
|
41
|
+
expect(expandSourcemapPaths(tree()).every((t) => t.name.endsWith('.map'))).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('leaves a single file as its basename', () => {
|
|
45
|
+
const root = tree()
|
|
46
|
+
const out = expandSourcemapPaths(join(root, 'pages/cart/index.js.map'))
|
|
47
|
+
expect(out).toHaveLength(1)
|
|
48
|
+
expect(out[0]!.name).toBe('index.js.map')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('does not throw on a path that does not exist', () => {
|
|
52
|
+
// The caller's read produces the real error with the real path in
|
|
53
|
+
// it; this must not pre-empt it with a worse one.
|
|
54
|
+
expect(() => expandSourcemapPaths('/nope/missing.map')).not.toThrow()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
|
|
8
|
+
const hook = fileURLToPath(new URL('../../../react-native/bin/sentori-rn-upload-source-bundle.cjs', import.meta.url))
|
|
9
|
+
let root: string
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), 'sentori-hook-test-')))
|
|
12
|
+
mkdirSync(join(root, 'node_modules/.bin'), { recursive: true })
|
|
13
|
+
writeFileSync(join(root, 'node_modules/.bin/sentori'), `#!/usr/bin/env node
|
|
14
|
+
require('node:fs').writeFileSync('invocation.json', JSON.stringify({args:process.argv.slice(2),token:process.env.BUILD_TOKEN}));process.exit(Number(process.env.STUB_EXIT || 0));`, { mode: 0o755 })
|
|
15
|
+
})
|
|
16
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }))
|
|
17
|
+
function run(args: string[], extra: Record<string,string> = {}) {
|
|
18
|
+
const env = { ...process.env, ...extra }; delete env.SENTORI_RELEASE
|
|
19
|
+
return spawnSync('node', [hook, ...args], { cwd: root, env, encoding: 'utf8' })
|
|
20
|
+
}
|
|
21
|
+
test('RN hook delegates the entire configured upload to sentori and keeps credentials out of argv', () => {
|
|
22
|
+
writeFileSync(join(root, 'sentori.config.json'), JSON.stringify({ version: 1, tokenEnv: 'BUILD_TOKEN' }))
|
|
23
|
+
expect(run(['--release', 'app@1+7', '--git-tag', 'v1', '--token', 'secret', '--strict']).status).toBe(0)
|
|
24
|
+
const invocation = JSON.parse(readFileSync(join(root, 'invocation.json'), 'utf8'))
|
|
25
|
+
expect(invocation.args).toEqual(['upload', 'all', '--config', join(root, 'sentori.config.json'), '--release', 'app@1+7', '--git-tag', 'v1', '--strict'])
|
|
26
|
+
expect(invocation.token).toBe('secret')
|
|
27
|
+
expect(run([], { STUB_EXIT: '1' }).status).toBe(0)
|
|
28
|
+
expect(run(['--strict'], { STUB_EXIT: '1' }).status).toBe(1)
|
|
29
|
+
})
|
|
30
|
+
test('legacy hook requires exact release identity instead of guessing package version', () => {
|
|
31
|
+
writeFileSync(join(root, 'package.json'), JSON.stringify({ version: '1.0.0' }))
|
|
32
|
+
expect(run(['--platform', 'ios', '--strict']).status).toBe(1)
|
|
33
|
+
expect(run(['--platform', 'ios', '--release', 'app@1+7', '--strict']).status).toBe(0)
|
|
34
|
+
expect(JSON.parse(readFileSync(join(root, 'invocation.json'), 'utf8')).args).toEqual(['upload', 'srcbundle', '--release', 'app@1+7', '--root', root, 'ios', '--strict'])
|
|
35
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { unzipSync, zipSync } from 'fflate'
|
|
7
|
+
import { collectSources, inspectSourceArchive, packSources, SOURCE_MANIFEST } from '../srcbundle.js'
|
|
8
|
+
|
|
9
|
+
let root: string
|
|
10
|
+
beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'sentori-sources-test-')) })
|
|
11
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }))
|
|
12
|
+
function file(path: string, body: string) { const full = join(root, path); mkdirSync(join(full, '..'), { recursive: true }); writeFileSync(full, body) }
|
|
13
|
+
function git(...args: string[]) { return execFileSync('git', ['-C', root, '-c', 'user.name=Test', '-c', 'user.email=test@example.test', '-c', 'commit.gpgsign=false', ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim() }
|
|
14
|
+
|
|
15
|
+
describe('source archive collection', () => {
|
|
16
|
+
test('works without Git, preserves selected roots and excludes dependencies and secrets', () => {
|
|
17
|
+
file('ios/Pay.swift', 'ios code'); file('shared/Pay.swift', 'shared code')
|
|
18
|
+
file('ios/.env', 'secret'); file('ios/Pods/P.swift', 'dependency')
|
|
19
|
+
file('shared/tests/Test.swift', 'test code'); file('shared/index.ts', 'export {}')
|
|
20
|
+
const result = packSources(['ios', 'shared'], 'app@1+7', { root, exclude: ['shared/tests'] })
|
|
21
|
+
expect(result.manifest.files.map(x => x.path)).toEqual(['ios/Pay.swift', 'shared/Pay.swift', 'shared/index.ts'])
|
|
22
|
+
expect(inspectSourceArchive(result.bytes, 'app@1+7').files).toEqual(result.manifest.files.map(x => x.path))
|
|
23
|
+
expect(unzipSync(result.bytes)['shared/Pay.swift']).toEqual(Buffer.from('shared code'))
|
|
24
|
+
expect(packSources(['shared', 'ios'], 'app@1+7', { root, exclude: ['shared/tests'] }).bytes).toEqual(result.bytes)
|
|
25
|
+
})
|
|
26
|
+
test('explicitly selected generated sources are included without sweeping build output', () => {
|
|
27
|
+
file('src/a.kt', 'app'); file('build/generated/Bindings.kt', 'generated'); file('build/cache/Other.kt', 'cache')
|
|
28
|
+
expect(Object.keys(collectSources(['.'], { root }).bundle)).toEqual(['src/a.kt'])
|
|
29
|
+
expect(Object.keys(collectSources(['src', 'build/generated'], { root }).bundle)).toEqual(['src/a.kt', 'build/generated/Bindings.kt'])
|
|
30
|
+
})
|
|
31
|
+
test('an exact annotated tag ignores later commits and uncommitted files without checkout', () => {
|
|
32
|
+
git('init'); file('src/a.kt', 'tagged code'); git('add', '.'); git('commit', '-m', 'tagged')
|
|
33
|
+
git('-c', 'tag.gpgsign=false', 'tag', '-a', 'v1', '-m', 'release')
|
|
34
|
+
const commit = git('rev-parse', 'HEAD')
|
|
35
|
+
file('src/a.kt', 'later code'); git('add', '.'); git('commit', '-m', 'later')
|
|
36
|
+
file('src/a.kt', 'uncommitted'); file('src/new.kt', 'untracked')
|
|
37
|
+
const head = git('rev-parse', 'HEAD')
|
|
38
|
+
const packed = packSources(['src'], 'app@1+7', { root, gitTag: 'v1' })
|
|
39
|
+
expect(packed.manifest.source).toEqual({ type: 'git-tag', tag: 'v1', commit })
|
|
40
|
+
expect(unzipSync(packed.bytes)['src/a.kt']).toEqual(Buffer.from('tagged code'))
|
|
41
|
+
expect(packed.manifest.files).toHaveLength(1)
|
|
42
|
+
expect(readFileSync(join(root, 'src/a.kt'), 'utf8')).toBe('uncommitted')
|
|
43
|
+
expect(git('rev-parse', 'HEAD')).toBe(head)
|
|
44
|
+
expect(() => packSources(['src'], 'r', { root, gitTag: 'master' })).toThrow()
|
|
45
|
+
})
|
|
46
|
+
test('skips symbolic links and rejects selections escaping the declared root', () => {
|
|
47
|
+
file('src/a.kt', 'ok'); symlinkSync('/etc/passwd', join(root, 'src/secret.kt'))
|
|
48
|
+
expect(Object.keys(collectSources(['src'], { root }).bundle)).toEqual(['src/a.kt'])
|
|
49
|
+
expect(() => collectSources(['../'], { root })).toThrow('outside')
|
|
50
|
+
})
|
|
51
|
+
test('does not silently truncate oversized selected sources', () => {
|
|
52
|
+
file('src/huge.kt', 'x'.repeat(512 * 1024 + 1))
|
|
53
|
+
expect(() => packSources(['src'], 'r', { root })).toThrow('512 KiB')
|
|
54
|
+
})
|
|
55
|
+
test('validates external ZIP paths, release and manifest hashes', () => {
|
|
56
|
+
file('src/a.kt', 'code')
|
|
57
|
+
const packed = packSources(['src'], 'r1', { root })
|
|
58
|
+
expect(() => inspectSourceArchive(packed.bytes, 'r2')).toThrow('does not match')
|
|
59
|
+
const files = unzipSync(packed.bytes); files['src/a.kt'] = Buffer.from('tampered')
|
|
60
|
+
expect(() => inspectSourceArchive(zipSync(files), 'r1')).toThrow('checksum')
|
|
61
|
+
expect(() => inspectSourceArchive(zipSync({ '../bad.kt': Buffer.from('bad') }))).toThrow('unsafe')
|
|
62
|
+
expect(() => inspectSourceArchive(zipSync({}))).toThrow('empty')
|
|
63
|
+
expect(inspectSourceArchive(zipSync({ 'Sources/App.swift': Buffer.from('plain zip') })).files).toEqual(['Sources/App.swift'])
|
|
64
|
+
expect(unzipSync(packed.bytes)[SOURCE_MANIFEST]).toBeDefined()
|
|
65
|
+
})
|
|
66
|
+
})
|