@apex-actions/cli 0.86.0 → 0.87.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/dist/cli/lsp/main.js +2 -1
- package/dist/cli/main.js +4 -23
- package/dist/cli/migrate/main.js +4 -3
- package/dist/cli/run/main.js +2 -1
- package/dist/cli/runner/main.js +2 -1
- package/dist/cli/validate/main.js +2 -1
- package/dist/src/infrastructure/binary-install.js +147 -0
- package/dist/src/infrastructure/binary-install.test.js +114 -0
- package/package.json +1 -1
package/dist/cli/lsp/main.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { serveLanguageServer } from '../../src/application/serve-language-server.js';
|
|
4
4
|
import { EngineBinaryLanguageServer } from '../../src/infrastructure/engine-binary.js';
|
|
5
|
+
import { resolveBinary } from '../../src/infrastructure/binary-install.js';
|
|
5
6
|
/**
|
|
6
7
|
* `apex lsp` — the language server, for an editor to start.
|
|
7
8
|
*
|
|
@@ -33,7 +34,7 @@ export default new Command('lsp')
|
|
|
33
34
|
.description('Run the workflow language server on stdio, for an editor to start as a child process')
|
|
34
35
|
.addHelpText('after', EDITOR_HELP)
|
|
35
36
|
.action(async () => {
|
|
36
|
-
const outcome = await serveLanguageServer(new EngineBinaryLanguageServer());
|
|
37
|
+
const outcome = await serveLanguageServer(new EngineBinaryLanguageServer(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN')));
|
|
37
38
|
if (outcome.kind === 'engine-missing') {
|
|
38
39
|
// Named as itself: an editor shows a non-zero exit as "the server
|
|
39
40
|
// crashed", which sends somebody looking in entirely the wrong place.
|
package/dist/cli/main.js
CHANGED
|
@@ -1,25 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { packageVersion } from '../src/infrastructure/binary-install.js';
|
|
4
3
|
import { buildProgram } from './program.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* `VERSION` is the source of truth in the repository, and it is not in the published package —
|
|
9
|
-
* nor should it be. What ships is `package.json`, npm guarantees it is there, and `version:check`
|
|
10
|
-
* guarantees it agrees with `VERSION`. Reading it by walking up also survives the layout changing
|
|
11
|
-
* underfoot: `cli/main.ts` under tsx and `dist/cli/main.js` from a tarball sit at different depths,
|
|
12
|
-
* and the previous `../VERSION` was correct for exactly one of them.
|
|
13
|
-
*/
|
|
14
|
-
const readVersion = () => {
|
|
15
|
-
for (let dir = import.meta.dirname;; dir = dirname(dir)) {
|
|
16
|
-
try {
|
|
17
|
-
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
|
-
if (dirname(dir) === dir)
|
|
21
|
-
return '0.0.0-unknown';
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
buildProgram(readVersion()).parse(process.argv);
|
|
4
|
+
// The version from the nearest `package.json` — what ships, and what `version:check` keeps equal to
|
|
5
|
+
// `VERSION` (see `packageVersion`); it also names which engine and runner the CLI downloads.
|
|
6
|
+
buildProgram(packageVersion()).parse(process.argv);
|
package/dist/cli/migrate/main.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Command } from 'commander';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { scanOrganization } from '../../src/application/scan-migration.js';
|
|
5
5
|
import { EngineBinaryCompat, LocalWorkflowSource } from '../../src/infrastructure/engine-binary.js';
|
|
6
|
+
import { resolveBinary } from '../../src/infrastructure/binary-install.js';
|
|
6
7
|
import { GitHubApi, NoTokenError } from '../../src/infrastructure/github-api.js';
|
|
7
8
|
import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
|
|
8
9
|
import { parseRepository } from '../../src/domain/runs.js';
|
|
@@ -118,8 +119,8 @@ const printScan = (scan, limit) => {
|
|
|
118
119
|
* first thing somebody evaluating a migration wants: an answer about a
|
|
119
120
|
* repository they already have, without handing anybody credentials.
|
|
120
121
|
*/
|
|
121
|
-
const deps = (options) => {
|
|
122
|
-
const checker = new EngineBinaryCompat();
|
|
122
|
+
const deps = (options, engine) => {
|
|
123
|
+
const checker = new EngineBinaryCompat(engine);
|
|
123
124
|
if (options.path !== undefined)
|
|
124
125
|
return { workflows: new LocalWorkflowSource(options.path), checker };
|
|
125
126
|
try {
|
|
@@ -138,7 +139,7 @@ const handler = async (org, options) => {
|
|
|
138
139
|
process.exitCode = 2;
|
|
139
140
|
return;
|
|
140
141
|
}
|
|
141
|
-
const built = deps(options);
|
|
142
|
+
const built = deps(options, await resolveBinary('apex-engine', 'APEX_ENGINE_BIN'));
|
|
142
143
|
if (built === undefined)
|
|
143
144
|
return;
|
|
144
145
|
// A local scan has no organization. The directory's name is what the person
|
package/dist/cli/run/main.js
CHANGED
|
@@ -2,10 +2,11 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { runWorkflow } from '../../src/application/run-workflow.js';
|
|
4
4
|
import { RunnerBinaryExecutor } from '../../src/infrastructure/runner-binary.js';
|
|
5
|
+
import { resolveBinary } from '../../src/infrastructure/binary-install.js';
|
|
5
6
|
import { GitHubWorkflowLocator } from '../../src/infrastructure/engine-binary.js';
|
|
6
7
|
const collect = (value, previous) => [...previous, value];
|
|
7
8
|
const handler = async (workflow, options) => {
|
|
8
|
-
const outcome = await runWorkflow(new RunnerBinaryExecutor(), new GitHubWorkflowLocator(), workflow, {
|
|
9
|
+
const outcome = await runWorkflow(new RunnerBinaryExecutor(await resolveBinary('apex-runner', 'APEX_RUNNER_BIN')), new GitHubWorkflowLocator(), workflow, {
|
|
9
10
|
...(options.job !== undefined ? { job: options.job } : {}),
|
|
10
11
|
...(options.event !== undefined ? { event: options.event } : {}),
|
|
11
12
|
...(options.payload !== undefined ? { payload: options.payload } : {}),
|
package/dist/cli/runner/main.js
CHANGED
|
@@ -4,6 +4,7 @@ import { fleetHistory, listRunners, mintRegistrationToken, registerRunner, remov
|
|
|
4
4
|
import { humanSeconds, parseDuration } from '../../src/domain/fleet-history.js';
|
|
5
5
|
import { HttpControlPlane, NotConfiguredError } from '../../src/infrastructure/control-plane-http.js';
|
|
6
6
|
import { RunnerBinaryExecutor } from '../../src/infrastructure/runner-binary.js';
|
|
7
|
+
import { resolveBinary } from '../../src/infrastructure/binary-install.js';
|
|
7
8
|
/** Build the API client, or explain what is missing and stop. */
|
|
8
9
|
const api = () => {
|
|
9
10
|
try {
|
|
@@ -142,7 +143,7 @@ const registerCommand = new Command('register')
|
|
|
142
143
|
// This process becomes the runner: it runs until stopped, and executes
|
|
143
144
|
// other people's code on this machine. Say so before it starts.
|
|
144
145
|
console.error(chalk.dim(`registering ${opts.name ?? 'this machine'} with ${opts.server} as [${opts.labels}]`));
|
|
145
|
-
const outcome = await registerRunner(client, new RunnerBinaryExecutor(), {
|
|
146
|
+
const outcome = await registerRunner(client, new RunnerBinaryExecutor(await resolveBinary('apex-runner', 'APEX_RUNNER_BIN')), {
|
|
146
147
|
server: opts.server,
|
|
147
148
|
labels: opts.labels.split(','),
|
|
148
149
|
name: opts.name,
|
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { validateWorkflows } from '../../src/application/validate-workflows.js';
|
|
4
4
|
import { EngineBinaryValidator, GitHubWorkflowLocator } from '../../src/infrastructure/engine-binary.js';
|
|
5
|
+
import { resolveBinary } from '../../src/infrastructure/binary-install.js';
|
|
5
6
|
import { diagnosticCount } from '../../src/domain/validation.js';
|
|
6
7
|
const printHuman = (report) => {
|
|
7
8
|
for (const file of report.files) {
|
|
@@ -21,7 +22,7 @@ const printHuman = (report) => {
|
|
|
21
22
|
console.log(chalk.dim(`${n} diagnostic${n === 1 ? '' : 's'}`));
|
|
22
23
|
};
|
|
23
24
|
const handler = async (files, options) => {
|
|
24
|
-
const outcome = await validateWorkflows(new EngineBinaryValidator(), new GitHubWorkflowLocator(), files);
|
|
25
|
+
const outcome = await validateWorkflows(new EngineBinaryValidator(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN')), new GitHubWorkflowLocator(), files);
|
|
25
26
|
switch (outcome.kind) {
|
|
26
27
|
case 'no-workflows':
|
|
27
28
|
console.error(chalk.yellow('no workflow files found (.github/workflows/*.yml) — pass files explicitly'));
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// infrastructure layer — see ../../../../docs/conventions.md §1
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { access, chmod, mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { constants } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { delimiter, dirname, join } from 'node:path';
|
|
9
|
+
import { promisify } from 'node:util';
|
|
10
|
+
/**
|
|
11
|
+
* Where `engine` and `runner` publish their binaries on a release tag (context ADR-0040): one archive per
|
|
12
|
+
* platform and a SHA256SUMS file per command, under the lockstep version.
|
|
13
|
+
*/
|
|
14
|
+
export const DIST_BASE = 'https://apexactions.com/dl';
|
|
15
|
+
/**
|
|
16
|
+
* The CLI's version, from the nearest `package.json` above this file.
|
|
17
|
+
*
|
|
18
|
+
* `VERSION` is the source of truth in the repository and is not in the published package; what ships is
|
|
19
|
+
* `package.json`, and `version:check` guarantees the two agree. Walking up survives the layout differing
|
|
20
|
+
* between `tsx` (`src/…`) and a tarball (`dist/src/…`).
|
|
21
|
+
*/
|
|
22
|
+
export const packageVersion = (from = import.meta.dirname) => {
|
|
23
|
+
for (let dir = from;; dir = dirname(dir)) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
if (dirname(dir) === dir)
|
|
29
|
+
return '0.0.0-unknown';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/** Node's names to Go's, for the four targets that are published; anything else has no download. */
|
|
34
|
+
export const platformOf = (platform, arch) => {
|
|
35
|
+
const os = platform === 'darwin' ? 'darwin' : platform === 'linux' ? 'linux' : undefined;
|
|
36
|
+
const goArch = arch === 'arm64' ? 'arm64' : arch === 'x64' ? 'amd64' : undefined;
|
|
37
|
+
return os !== undefined && goArch !== undefined ? { os, arch: goArch } : undefined;
|
|
38
|
+
};
|
|
39
|
+
export const archiveName = (command, version, p) => `${command}_${version}_${p.os}_${p.arch}.tar.gz`;
|
|
40
|
+
export const sumsName = (command, version) => `${command}_${version}_SHA256SUMS`;
|
|
41
|
+
/** The checksum `sha256sum` printed for one file, or undefined when the file is not listed. */
|
|
42
|
+
export const expectedSum = (sums, file) => {
|
|
43
|
+
for (const line of sums.split('\n')) {
|
|
44
|
+
const match = /^([0-9a-f]{64})\s+\*?(\S+)$/.exec(line.trim());
|
|
45
|
+
if (match?.[2] === file)
|
|
46
|
+
return match[1];
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
/** A release version has a download; a development build (`0.0.0-unknown`, a prerelease) does not. */
|
|
51
|
+
const published = (version) => /^\d+\.\d+\.\d+$/.test(version);
|
|
52
|
+
const isExecutable = async (path) => {
|
|
53
|
+
try {
|
|
54
|
+
await access(path, constants.X_OK);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
const onPath = async (command, env) => {
|
|
62
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
63
|
+
if (dir !== '' && (await isExecutable(join(dir, command))))
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
const run = promisify(execFile);
|
|
69
|
+
export const defaultInstallDeps = () => ({
|
|
70
|
+
env: process.env,
|
|
71
|
+
version: packageVersion(),
|
|
72
|
+
platform: platformOf(process.platform, process.arch),
|
|
73
|
+
cacheDir: join(process.env.APEX_HOME ?? join(homedir(), '.apex'), 'bin'),
|
|
74
|
+
distBase: process.env.APEX_DIST_URL ?? DIST_BASE,
|
|
75
|
+
fetch: async (url) => {
|
|
76
|
+
const res = await fetch(url);
|
|
77
|
+
return { ok: res.ok, status: res.status, bytes: async () => new Uint8Array(await res.arrayBuffer()) };
|
|
78
|
+
},
|
|
79
|
+
extract: async (archive, into) => {
|
|
80
|
+
await run('tar', ['-xzf', archive, '-C', into]);
|
|
81
|
+
},
|
|
82
|
+
notice: (message) => process.stderr.write(`${message}\n`),
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* The path to run for `command` (`apex-engine`, `apex-runner`), installing it when nothing else provides one.
|
|
86
|
+
*
|
|
87
|
+
* In order: the override in `envVar`; the command on `PATH`; this version's copy in the cache
|
|
88
|
+
* (`~/.apex/bin/<version>/`); and otherwise this version's archive from {@link DIST_BASE}, checked against the
|
|
89
|
+
* published SHA256SUMS before it is unpacked. The version is the CLI's own, so the CLI never runs an engine
|
|
90
|
+
* from another release. When nothing works the bare command is returned and the adapter reports it missing,
|
|
91
|
+
* after a notice that says why the download did not happen.
|
|
92
|
+
*/
|
|
93
|
+
export const resolveBinary = async (command, envVar, deps = defaultInstallDeps()) => {
|
|
94
|
+
const override = deps.env[envVar];
|
|
95
|
+
if (override !== undefined && override !== '')
|
|
96
|
+
return override;
|
|
97
|
+
if (await onPath(command, deps.env))
|
|
98
|
+
return command;
|
|
99
|
+
const dir = join(deps.cacheDir, deps.version);
|
|
100
|
+
const cached = join(dir, command);
|
|
101
|
+
if (await isExecutable(cached))
|
|
102
|
+
return cached;
|
|
103
|
+
if (!published(deps.version))
|
|
104
|
+
return command;
|
|
105
|
+
if (deps.platform === undefined) {
|
|
106
|
+
deps.notice(`${command} is published for macOS and Linux on arm64 and x64 only; install it yourself and set ${envVar}`);
|
|
107
|
+
return command;
|
|
108
|
+
}
|
|
109
|
+
const archive = archiveName(command, deps.version, deps.platform);
|
|
110
|
+
const base = `${deps.distBase.replace(/\/+$/, '')}/${deps.version}`;
|
|
111
|
+
try {
|
|
112
|
+
deps.notice(`Installing ${command} ${deps.version} for ${deps.platform.os}/${deps.platform.arch} into ${dir}`);
|
|
113
|
+
const sums = await deps.fetch(`${base}/${sumsName(command, deps.version)}`);
|
|
114
|
+
if (!sums.ok)
|
|
115
|
+
throw new Error(`checksums: HTTP ${sums.status}`);
|
|
116
|
+
const want = expectedSum(new TextDecoder().decode(await sums.bytes()), archive);
|
|
117
|
+
if (want === undefined)
|
|
118
|
+
throw new Error(`${archive} is not in the published checksums`);
|
|
119
|
+
const body = await deps.fetch(`${base}/${archive}`);
|
|
120
|
+
if (!body.ok)
|
|
121
|
+
throw new Error(`${archive}: HTTP ${body.status}`);
|
|
122
|
+
const bytes = await body.bytes();
|
|
123
|
+
const got = createHash('sha256').update(bytes).digest('hex');
|
|
124
|
+
if (got !== want)
|
|
125
|
+
throw new Error(`${archive} does not match its published checksum`);
|
|
126
|
+
// Unpack inside the cache (one filesystem, so the move is a rename) and move into place, so a
|
|
127
|
+
// half-written install is never found as a cached one.
|
|
128
|
+
await mkdir(deps.cacheDir, { recursive: true });
|
|
129
|
+
const work = await mkdtemp(join(deps.cacheDir, `.${command}-`));
|
|
130
|
+
try {
|
|
131
|
+
const file = join(work, archive);
|
|
132
|
+
await writeFile(file, bytes);
|
|
133
|
+
await deps.extract(file, work);
|
|
134
|
+
await chmod(join(work, command), 0o755);
|
|
135
|
+
await mkdir(dir, { recursive: true });
|
|
136
|
+
await rename(join(work, command), cached);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await rm(work, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
return cached;
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
deps.notice(`Could not install ${command} from ${base}/: ${error.message}`);
|
|
145
|
+
return command;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
import { archiveName, defaultInstallDeps, DIST_BASE, expectedSum, packageVersion, platformOf, resolveBinary, sumsName, } from './binary-install.js';
|
|
8
|
+
const VERSION = '0.87.0';
|
|
9
|
+
const PLATFORM = { os: 'darwin', arch: 'arm64' };
|
|
10
|
+
/** A real archive, laid out the way binary-publish packs one: the command, the licence and the notices. */
|
|
11
|
+
const makeArchive = (command) => {
|
|
12
|
+
const stage = mkdtempSync(join(tmpdir(), 'stage-'));
|
|
13
|
+
writeFileSync(join(stage, command), '#!/bin/sh\necho engine\n');
|
|
14
|
+
chmodSync(join(stage, command), 0o755);
|
|
15
|
+
writeFileSync(join(stage, 'LICENSE'), 'licence');
|
|
16
|
+
writeFileSync(join(stage, 'THIRD-PARTY-NOTICES.md'), 'notices');
|
|
17
|
+
const out = join(stage, 'a.tar.gz');
|
|
18
|
+
execFileSync('tar', ['-czf', out, '-C', stage, command, 'LICENSE', 'THIRD-PARTY-NOTICES.md']);
|
|
19
|
+
return new Uint8Array(readFileSync(out));
|
|
20
|
+
};
|
|
21
|
+
const sha = (b) => createHash('sha256').update(b).digest('hex');
|
|
22
|
+
const harness = (over = {}) => {
|
|
23
|
+
const notices = [];
|
|
24
|
+
const fetched = [];
|
|
25
|
+
const files = over.files ?? {};
|
|
26
|
+
const deps = {
|
|
27
|
+
...defaultInstallDeps(),
|
|
28
|
+
env: { PATH: '' },
|
|
29
|
+
version: VERSION,
|
|
30
|
+
platform: PLATFORM,
|
|
31
|
+
cacheDir: mkdtempSync(join(tmpdir(), 'apex-bin-')),
|
|
32
|
+
fetch: async (url) => {
|
|
33
|
+
fetched.push(url);
|
|
34
|
+
const body = files[url];
|
|
35
|
+
if (body === undefined)
|
|
36
|
+
return { ok: false, status: 404, bytes: async () => new Uint8Array() };
|
|
37
|
+
return { ok: true, status: 200, bytes: async () => (typeof body === 'string' ? new TextEncoder().encode(body) : body) };
|
|
38
|
+
},
|
|
39
|
+
notice: (m) => notices.push(m),
|
|
40
|
+
...over,
|
|
41
|
+
};
|
|
42
|
+
return { deps, notices, fetched };
|
|
43
|
+
};
|
|
44
|
+
const published = (command, archive, sum = sha(archive)) => ({
|
|
45
|
+
[`${DIST_BASE}/${VERSION}/${sumsName(command, VERSION)}`]: `${sum} ${archiveName(command, VERSION, PLATFORM)}\n`,
|
|
46
|
+
[`${DIST_BASE}/${VERSION}/${archiveName(command, VERSION, PLATFORM)}`]: archive,
|
|
47
|
+
});
|
|
48
|
+
describe('resolveBinary', () => {
|
|
49
|
+
it('prefers the override, then PATH, and downloads nothing for either', async () => {
|
|
50
|
+
const { deps, fetched } = harness({ env: { APEX_ENGINE_BIN: '/opt/engine', PATH: '' } });
|
|
51
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', deps)).toBe('/opt/engine');
|
|
52
|
+
const bin = mkdtempSync(join(tmpdir(), 'path-'));
|
|
53
|
+
writeFileSync(join(bin, 'apex-engine'), '');
|
|
54
|
+
chmodSync(join(bin, 'apex-engine'), 0o755);
|
|
55
|
+
const onPath = harness({ env: { PATH: bin } });
|
|
56
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', onPath.deps)).toBe('apex-engine');
|
|
57
|
+
expect([...fetched, ...onPath.fetched]).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
it('downloads this version for this platform, checks it, unpacks it executable, and reuses it', async () => {
|
|
60
|
+
const archive = makeArchive('apex-engine');
|
|
61
|
+
const { deps, fetched } = harness({ files: published('apex-engine', archive) });
|
|
62
|
+
const path = await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', deps);
|
|
63
|
+
expect(path).toBe(join(deps.cacheDir, VERSION, 'apex-engine'));
|
|
64
|
+
expect(statSync(path).mode & 0o111).not.toBe(0);
|
|
65
|
+
expect(execFileSync(path).toString()).toBe('engine\n');
|
|
66
|
+
expect(fetched).toEqual([
|
|
67
|
+
`${DIST_BASE}/${VERSION}/apex-engine_${VERSION}_SHA256SUMS`,
|
|
68
|
+
`${DIST_BASE}/${VERSION}/apex-engine_${VERSION}_darwin_arm64.tar.gz`,
|
|
69
|
+
]);
|
|
70
|
+
// The second call finds the cached copy and fetches nothing.
|
|
71
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', deps)).toBe(path);
|
|
72
|
+
expect(fetched).toHaveLength(2);
|
|
73
|
+
});
|
|
74
|
+
it('refuses an archive that does not match its published checksum, and installs nothing', async () => {
|
|
75
|
+
const archive = makeArchive('apex-engine');
|
|
76
|
+
const { deps, notices } = harness({ files: published('apex-engine', archive, 'f'.repeat(64)) });
|
|
77
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', deps)).toBe('apex-engine');
|
|
78
|
+
expect(notices.at(-1)).toMatch(/does not match its published checksum/);
|
|
79
|
+
expect(() => statSync(join(deps.cacheDir, VERSION, 'apex-engine'))).toThrow();
|
|
80
|
+
});
|
|
81
|
+
it('says why when the release has no download, and falls back to the bare command', async () => {
|
|
82
|
+
const { deps, notices } = harness();
|
|
83
|
+
expect(await resolveBinary('apex-runner', 'APEX_RUNNER_BIN', deps)).toBe('apex-runner');
|
|
84
|
+
expect(notices.at(-1)).toMatch(/Could not install apex-runner .*checksums: HTTP 404/);
|
|
85
|
+
});
|
|
86
|
+
it('never downloads for a development build or an unpublished platform', async () => {
|
|
87
|
+
const dev = harness({ version: '0.0.0-unknown' });
|
|
88
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', dev.deps)).toBe('apex-engine');
|
|
89
|
+
expect(dev.fetched).toEqual([]);
|
|
90
|
+
const windows = harness({ platform: undefined });
|
|
91
|
+
expect(await resolveBinary('apex-engine', 'APEX_ENGINE_BIN', windows.deps)).toBe('apex-engine');
|
|
92
|
+
expect(windows.fetched).toEqual([]);
|
|
93
|
+
expect(windows.notices.at(-1)).toMatch(/macOS and Linux on arm64 and x64 only/);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('the names', () => {
|
|
97
|
+
it('maps Node platforms to the published Go targets', () => {
|
|
98
|
+
expect(platformOf('darwin', 'arm64')).toEqual({ os: 'darwin', arch: 'arm64' });
|
|
99
|
+
expect(platformOf('linux', 'x64')).toEqual({ os: 'linux', arch: 'amd64' });
|
|
100
|
+
expect(platformOf('win32', 'x64')).toBeUndefined();
|
|
101
|
+
expect(platformOf('linux', 'ia32')).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('reads one line of sha256sum output, with or without the binary-mode star', () => {
|
|
104
|
+
const sums = `${'a'.repeat(64)} apex-engine_1.0.0_linux_amd64.tar.gz\n${'b'.repeat(64)} *apex-engine_1.0.0_darwin_arm64.tar.gz\n`;
|
|
105
|
+
expect(expectedSum(sums, 'apex-engine_1.0.0_darwin_arm64.tar.gz')).toBe('b'.repeat(64));
|
|
106
|
+
expect(expectedSum(sums, 'missing.tar.gz')).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it('reads the version from the nearest package.json', () => {
|
|
109
|
+
const root = mkdtempSync(join(tmpdir(), 'pkg-'));
|
|
110
|
+
mkdirSync(join(root, 'dist', 'src'), { recursive: true });
|
|
111
|
+
writeFileSync(join(root, 'package.json'), JSON.stringify({ version: '9.8.7' }));
|
|
112
|
+
expect(packageVersion(join(root, 'dist', 'src'))).toBe('9.8.7');
|
|
113
|
+
});
|
|
114
|
+
});
|