@journeyapps/cloudcode-build-agent 0.0.0-dev.8ebefb0 → 0.0.0-dev.90a9f01

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/src/builder.ts CHANGED
@@ -1,105 +1,7 @@
1
- import * as path from 'node:path';
2
- import * as jetpack from 'fs-jetpack';
3
- import * as semver from 'semver';
4
- import * as utils from './installer';
5
- import { detectTasks } from './detect_tasks';
6
- import { runCommand } from './run';
1
+ import { spawnSync } from 'child_process';
7
2
 
8
- import * as _ from 'lodash';
3
+ // Detect tasks in given path and build them.
4
+ export async function buildTasks(src: string, dest: string, only?: string) {}
9
5
 
10
- export type GeneralTaskConfig = {
11
- app_id: string;
12
- env: string;
13
- backend_id: string;
14
- backend_url: string;
15
- };
16
-
17
- export async function buildTasks(project_path: string, dest: string, config: GeneralTaskConfig, only?: string) {
18
- const tasks = await detectTasks(project_path, only);
19
-
20
- const required_node_versions = _.compact(tasks.map((t) => t.required_node_version));
21
- const install_node_versions = _.uniq(required_node_versions);
22
-
23
- // FIXME: Maybe refactor this section into an ensureNodeVersion (or something) that returns the relevant toolpaths?
24
- const tool_paths: Record<string, { bin_path: string; node_bin: string; npm_bin: string }> = {};
25
- // Use the version of nodejs that's running this script as the default.
26
- const default_tool_paths = {
27
- bin_path: path.dirname(process.execPath),
28
- node_bin: process.execPath,
29
- npm_bin: path.join(path.dirname(process.execPath), 'npm')
30
- };
31
- tool_paths[process.version] = default_tool_paths;
32
-
33
- for (const required_node_version of install_node_versions) {
34
- // FIXME: put this in another directory to avoid leaking into the builder
35
- const custom_node_path = path.resolve(`node-${semver.major(required_node_version)}`);
36
-
37
- if (!jetpack.exists(path.join(custom_node_path, 'bin/node'))) {
38
- console.debug(`installing to ${custom_node_path}`);
39
- await jetpack.dirAsync(custom_node_path);
40
- await utils.downloadAndInstallNode(required_node_version, custom_node_path);
41
- } else {
42
- console.debug(`already installed in ${custom_node_path}`);
43
- }
44
- tool_paths[required_node_version] = utils.nodePaths(custom_node_path);
45
- }
46
- // console.debug('----');
47
-
48
- for (const task of tasks) {
49
- const node_version = task.required_node_version ?? process.version;
50
- const builder_package = task.builder_package;
51
- const builder_script = task.builder_script;
52
- const { bin_path, node_bin, npm_bin } = tool_paths[node_version];
53
- const builder_bin = path.resolve(bin_path, builder_script);
54
-
55
- if (!jetpack.exists(node_bin)) {
56
- throw new Error(`Node binary not found: ${node_bin}`);
57
- }
58
-
59
- console.debug(`[${task.task_name}] Installing builder script "${builder_package}" for node ${node_version}`);
60
- // debug(`Installing builder script "${builder_package}" for node ${node_version}`);
61
-
62
- const stream1 = runCommand(node_bin, [npm_bin, '--global', 'install', builder_package], {
63
- cwd: project_path,
64
- env: process.env
65
- });
66
- for await (let event of stream1) {
67
- const log = event.stdout ?? event.stderr;
68
- if (log) {
69
- console.log(`[${task.task_name} - install]`, log.trimRight());
70
- }
71
- }
72
-
73
- if (!jetpack.exists(builder_bin)) {
74
- console.error(`[${task.task_name}] ${builder_bin} not found`);
75
- throw new Error(`Builder script not found for task ${task.task_name}`);
76
- }
77
-
78
- const builder_args: string[] = [];
79
- // --src ./ --out ./dist/echo --task echo --appId 'appid' --env 'testing' --backendId 'backendid' --backendUrl 'http://run.journeyapps.test'
80
- builder_args.push(builder_bin);
81
- builder_args.push('--src', project_path);
82
- builder_args.push(`--out`, path.join(dest, task.task_name));
83
- builder_args.push(`--zip`, path.join(dest, `${task.task_name}.zip`));
84
- builder_args.push(`--task`, `${task.task_name}`);
85
- builder_args.push(`--appId`, config.app_id);
86
- builder_args.push(`--env`, config.env);
87
- builder_args.push(`--backendId`, config.backend_id);
88
- builder_args.push(`--backendUrl`, config.backend_url);
89
- // builder_args.push(`--runInstallScripts`, 'true'); // TODO: handle this from feature flags somehow
90
-
91
- console.debug(`[${task.task_name}] Trying: ${node_bin} ${builder_args.join(' ')}`);
92
- // debug(`Trying: ${node_bin} ${builder_args.join(' ')}`);
93
-
94
- const stream2 = runCommand(node_bin, builder_args, { cwd: project_path, env: process.env });
95
-
96
- for await (let event of stream2) {
97
- const log = event.stdout ?? event.stderr;
98
- if (log) {
99
- console.log(`[${task.task_name} - build]`, log.trimRight());
100
- }
101
- }
102
-
103
- // console.debug('----');
104
- }
105
- }
6
+ // Build a single task in the specified subdirectory.
7
+ async function buildTask(src: string, dest: string, config: any) {}
package/src/cli.ts CHANGED
@@ -2,36 +2,40 @@
2
2
 
3
3
  import * as yargs from 'yargs';
4
4
 
5
- import { buildTasks } from './';
5
+ import { ProcessError, buildTasks, prepareBuildEnvForTasks } from './';
6
6
 
7
7
  const DEFAULT_SRC_PATH = './';
8
- const DEFAULT_OUT_PATH = './dist';
8
+ const DEFAULT_OUT_PATH = '';
9
9
 
10
10
  yargs
11
11
  .command(
12
12
  ['build', '$0'], // $0 means this is the default command
13
13
  'build tasks',
14
14
  (yargs) => {
15
- return (
16
- yargs
17
- .option('only', { string: true })
18
- .option('path', { default: DEFAULT_SRC_PATH })
19
- .option('out', { default: DEFAULT_OUT_PATH })
20
- // TODO: investigate yargs.config() for reading task config blob from a file. But how to ensure required args?
21
- .option('appId', { string: true, demandOption: true, describe: "CloudCode's reference id" })
22
- .option('env', { string: true, demandOption: true })
23
- .option('backendId', { string: true, demandOption: true })
24
- .option('backendUrl', { string: true, demandOption: true })
25
- );
15
+ return yargs
16
+ .option('only', { string: true })
17
+ .option('path', { default: DEFAULT_SRC_PATH })
18
+ .option('out', { default: DEFAULT_OUT_PATH });
26
19
  },
27
20
  handled(async (argv) => {
28
- const config = {
29
- app_id: argv.appId,
30
- env: argv.env,
31
- backend_id: argv.backendId,
32
- backend_url: argv.backendUrl
33
- };
34
- await buildTasks(argv.path, argv.out, config, argv.only);
21
+ // iterate over task directories and run: 'yarn cloudcode-build'.
22
+ // optionally filtered by `argv.only` to build a single task.
23
+
24
+ await buildTasks(argv.path, argv.out, argv.only);
25
+ })
26
+ )
27
+ .command(
28
+ 'prepare-env',
29
+ 'Install node environments and packages for each task',
30
+ (yargs) => {
31
+ return yargs.option('only', { string: true }).option('path', { default: DEFAULT_SRC_PATH });
32
+ },
33
+ handled(async (argv) => {
34
+ // iterate over task directories:
35
+ // ensure required node version for task CC version is installed - What if too old - error?
36
+ // run yarn install
37
+
38
+ await prepareBuildEnvForTasks(argv.path, argv.only);
35
39
  })
36
40
  )
37
41
  .option('verbose', {
@@ -44,8 +48,13 @@ function handled<T>(fn: (argv: T) => Promise<void>): (argv: T) => Promise<void>
44
48
  try {
45
49
  await fn(argv);
46
50
  } catch (err) {
47
- console.error(err.stack);
48
- process.exit(1);
51
+ if (err instanceof ProcessError) {
52
+ console.error(err.message);
53
+ process.exit(err.status);
54
+ } else {
55
+ console.error(err.stack);
56
+ process.exit(1);
57
+ }
49
58
  }
50
59
  };
51
60
  }
package/src/errors.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { SpawnSyncReturns } from 'child_process';
2
+
3
+ export class ProcessError extends Error {
4
+ cause?: Error;
5
+ status: number;
6
+
7
+ constructor(public result: SpawnSyncReturns<string>) {
8
+ super(constructMessage(result));
9
+
10
+ if (result.error) {
11
+ this.cause = result.error;
12
+ }
13
+ this.status = result.status || 1;
14
+ }
15
+ }
16
+
17
+ function constructMessage(result: SpawnSyncReturns<string>): string {
18
+ if (result.error) {
19
+ return result.error.message;
20
+ }
21
+ return `${result.stdout}${result.stderr}`.trim();
22
+ }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
+ export * from './errors';
1
2
  export * from './builder';
3
+ export * from './installer';
package/src/installer.ts CHANGED
@@ -1,112 +1,88 @@
1
- import fetch from 'node-fetch';
2
- import * as path from 'node:path';
1
+ import { spawnSync } from 'child_process';
3
2
  import * as fs from 'node:fs';
4
- import * as os from 'node:os';
5
- import * as URL from 'node:url';
6
- import * as tar from 'tar';
7
-
8
- /* Basically this, but for different dirs.
9
- curl https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${PLATFORM}64.tar.gz -L | tar -xzC /node-dedicated && \
10
- mv /node-dedicated/node-v${NODE_VERSION}-linux-${PLATFORM}64/bin/node /node-dedicated/node
11
- */
12
- // TODO: feature to find the latest minor and patch for a given major version?
13
- export async function downloadAndInstallNode(node_version: string, destination: string) {
14
- // eg. https://nodejs.org/dist/v18.14.2/node-v18.14.2-linux-x64.tar.xz
15
-
16
- // const PLATFORM = 'x';
17
- // const node_download_url = `https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-${PLATFORM}64.tar.gz`;
18
-
19
- // TODO: this is just for dev for now. Find a better fix.
20
- const ARCH = os.arch();
21
- const PLATFORM = os.platform();
22
- const node_download_url = `https://nodejs.org/dist/v${node_version}/node-v${node_version}-${PLATFORM}-${ARCH}.tar.gz`;
23
-
24
- await downloadAndExtractTarball(node_download_url, destination);
25
- // TODO: move binaries into local bin path?
26
- return nodePaths(destination);
27
- }
28
-
29
- export function nodePaths(destination: string) {
30
- return {
31
- bin_path: path.resolve(destination, 'bin'),
32
- node_bin: path.resolve(destination, 'bin', 'node'),
33
- npm_bin: path.resolve(destination, 'bin', 'npm')
34
- };
35
- }
36
-
37
- export async function downloadAndExtractTarball(url: string, dest: string) {
38
- const tmpdir = os.tmpdir();
39
-
40
- const filename = path.basename(URL.parse(url).pathname as string);
41
- const base = path.basename(filename, '.tar.gz');
42
- const download = path.join(tmpdir, filename);
43
-
44
- if (fs.existsSync(download)) {
45
- console.debug(`deleting old ${download}`);
46
- fs.unlinkSync(download);
3
+ import * as path from 'node:path';
4
+ import * as jetpack from 'fs-jetpack';
5
+ import * as semver from 'semver';
6
+ import * as utils from './utils';
7
+
8
+ const SUPPORTED_VERSIONS = [
9
+ {
10
+ version: '1.12.0',
11
+ node: '16.19.1' // FIXME: this is maybe brittle?
12
+ },
13
+ {
14
+ version: '1.11.2',
15
+ node: '14.21.3'
16
+ },
17
+ {
18
+ version: '1.11.1',
19
+ node: '14.21.3'
20
+ },
21
+ {
22
+ version: '1.11.0',
23
+ node: '14.21.3'
47
24
  }
48
-
49
- await new Promise(async (resolve, reject) => {
50
- console.log(`fetching ${url} into ${download}`);
51
-
52
- const response = await fetch(url);
53
- if (!response.ok) {
54
- const errorBody = await response.statusText;
55
- throw new Error(`Failed to download: ${errorBody}`);
25
+ ];
26
+
27
+ export async function prepareBuildEnvForTasks(project_path: string, only?: string) {
28
+ const package_files = await jetpack.findAsync(path.join(project_path, 'cloudcode'), { matching: '**/package.json' });
29
+ const filtered_package_files = package_files.filter((pkg_path) => {
30
+ // FIXME: this is kinda clunky.
31
+ const pm = /^\/?cloudcode\/([^\/]+)\/package\.json$/.exec(pkg_path);
32
+ if (!pm) {
33
+ return false;
56
34
  }
35
+ const taskName = pm[1];
57
36
 
58
- const file = fs.createWriteStream(download); // Sink the download stream into a file, so it can be extracted.
59
- response.body.pipe(file);
37
+ if (only != null && only != taskName) {
38
+ // !(only.indexOf(taskName) >= 0)
39
+ return false;
40
+ }
41
+ return true;
42
+ });
60
43
 
61
- file.on('finish', async () => {
62
- try {
63
- console.log('Extracting...', download);
44
+ // FIXME: Would it be better to map out required node_versions and subsequently install them, or is this in-line approach okay?
45
+ /* TODO:
46
+ How to install and execute custom node version in a portable way?
47
+ - install to predictable path inside project dir, which is writeable in the container.
48
+ But how to run it from inside task dirs? local bin? ENV?
49
+ What about yarn for each node version?
50
+ */
51
+
52
+ for (const pkg_path of filtered_package_files) {
53
+ try {
54
+ const content = await fs.promises.readFile(pkg_path, { encoding: 'utf-8' });
55
+ const task_package = JSON.parse(content);
56
+ const task_version = task_package?.cloudcode?.runtime;
57
+
58
+ // check task version against supported versions, install relevant node version and yarn
59
+ console.log(`Detected task version ${task_version}`);
60
+
61
+ const matching = SUPPORTED_VERSIONS.find((v) => {
62
+ return semver.satisfies(v.version, task_version);
63
+ });
64
+ if (!matching) {
65
+ console.error('FIXME: unsupported version');
66
+ }
64
67
 
65
- // const comp = fs.createReadStream(download);
68
+ console.log(`Matching versions: ${JSON.stringify(matching)}`);
69
+ const running_node_version = process.versions.node;
70
+ if (matching?.node && semver.major(matching.node) !== semver.major(running_node_version)) {
71
+ console.log(`Task requires different node version: v${matching.node}`);
66
72
 
67
- tar.extract({
68
- cwd: tmpdir,
69
- file: download,
70
- sync: true
71
- });
73
+ const NODE_VERSION = matching.node;
72
74
 
73
- const uncomp = path.join(tmpdir, base);
75
+ const custom_node_path = path.resolve(`node-${semver.major(NODE_VERSION)}`);
74
76
 
75
- const dist = path.resolve(dest);
76
- // FIXME: dangerous. Add protection or replace.
77
- // if (fs.existsSync(dist)) {
78
- // fs.rmSync(dist, { recursive: true });
79
- // }
80
- if (fs.existsSync(uncomp)) {
81
- console.log(`Moving extracted files from ${uncomp} to ${dist}`);
82
- fs.renameSync(uncomp, dist);
83
- } else {
84
- console.log(`Can't find extract dir ${uncomp}`);
77
+ if (!jetpack.exists(custom_node_path)) {
78
+ await jetpack.dirAsync(custom_node_path);
79
+ await utils.downloadAndInstallNode(NODE_VERSION, custom_node_path);
85
80
  }
86
-
87
- // FIXME: this seems to sometimes cause errors
88
- /*
89
- ----
90
- node:events:505
91
- throw er; // Unhandled 'error' event
92
- ^
93
-
94
- Error: ENOENT: no such file or directory, open '/var/folders/kc/h6m4zpmd23v13s63mygvkslm0000gn/T/node-v16.19.1-darwin-arm64.tar.gz'
95
- Emitted 'error' event on ReadStream instance at:
96
- at emitErrorNT (node:internal/streams/destroy:157:8)
97
- at emitErrorCloseNT (node:internal/streams/destroy:122:3)
98
- at processTicksAndRejections (node:internal/process/task_queues:83:21) {
99
- errno: -2,
100
- code: 'ENOENT',
101
- syscall: 'open',
102
- path: '/var/folders/kc/h6m4zpmd23v13s63mygvkslm0000gn/T/node-v16.19.1-darwin-arm64.tar.gz'
103
- */
104
- // fs.unlinkSync(file.path);
105
-
106
- resolve(null);
107
- } catch (err) {
108
- reject(err);
109
81
  }
110
- });
111
- });
82
+ } catch (err) {
83
+ console.error(`ERROR: ${err}`);
84
+ // todo: check for enoent and skip?
85
+ // todo: if json error, throw a CC build error?
86
+ }
87
+ }
112
88
  }
package/src/utils.ts ADDED
@@ -0,0 +1,63 @@
1
+ import fetch from 'node-fetch';
2
+ import * as path from 'node:path';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as URL from 'node:url';
6
+ import * as tar from 'tar';
7
+
8
+ /* Basically this, but for different dirs.
9
+ curl https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${PLATFORM}64.tar.gz -L | tar -xzC /node-dedicated && \
10
+ mv /node-dedicated/node-v${NODE_VERSION}-linux-${PLATFORM}64/bin/node /node-dedicated/node
11
+ */
12
+ export async function downloadAndInstallNode(node_version: string, destination: string) {
13
+ // eg. https://nodejs.org/dist/v18.14.2/node-v18.14.2-linux-x64.tar.xz
14
+ const PLATFORM = 'x';
15
+ const node_download_url = `https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-${PLATFORM}64.tar.gz`;
16
+
17
+ await downloadAndExtractTarball(node_download_url, destination);
18
+ // TODO: move binaries into local bin path?
19
+ }
20
+
21
+ export async function downloadAndExtractTarball(url: string, dest: string) {
22
+ const tmpdir = os.tmpdir();
23
+
24
+ const filename = path.basename(URL.parse(url).pathname as string);
25
+ const base = path.basename(filename, '.tar.gz');
26
+ const download = path.join(tmpdir, filename);
27
+
28
+ const file = fs.createWriteStream(download);
29
+ const response = await fetch(url);
30
+ if (!response.ok) {
31
+ const errorBody = await response.text();
32
+ throw new Error(`Failed to download: ${errorBody}`);
33
+ }
34
+
35
+ // FIXME: replace with webstreams?
36
+ response.body.pipe(file);
37
+
38
+ file.on('finish', async () => {
39
+ console.log('Extracting...', download);
40
+
41
+ const comp = fs.createReadStream(download);
42
+
43
+ tar.extract({
44
+ cwd: tmpdir,
45
+ file: comp.path as string,
46
+ sync: true
47
+ });
48
+
49
+ const uncomp = path.join(tmpdir, base);
50
+
51
+ const dist = path.resolve(dest);
52
+ // FIXME: dangerous. Add protection or replace.
53
+ // if (fs.existsSync(dist)) {
54
+ // fs.rmSync(dist, { recursive: true });
55
+ // }
56
+ if (fs.existsSync(uncomp)) {
57
+ console.log(`Moving extracted files from ${uncomp} to ${dist}`);
58
+ fs.renameSync(uncomp, dist);
59
+ }
60
+
61
+ fs.unlinkSync(file.path);
62
+ });
63
+ }
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "compilerOptions": {
4
4
  "outDir": "dist",
5
5
  "declarationDir": "dist",
6
- "rootDir": "src"
6
+ "rootDir": "src",
7
+ "lib": ["es2019", "dom"],
7
8
  },
8
9
  }