gcp-job-runner 1.0.0-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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +289 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/cloud/deploy.mjs +285 -0
- package/dist/cloud/deploy.mjs.map +1 -0
- package/dist/cloud/dockerfile.mjs +41 -0
- package/dist/cloud/dockerfile.mjs.map +1 -0
- package/dist/cloud/execute.mjs +103 -0
- package/dist/cloud/execute.mjs.map +1 -0
- package/dist/cloud/execution-poller.mjs +94 -0
- package/dist/cloud/execution-poller.mjs.map +1 -0
- package/dist/cloud/gcloud.mjs +125 -0
- package/dist/cloud/gcloud.mjs.map +1 -0
- package/dist/cloud/hash.mjs +44 -0
- package/dist/cloud/hash.mjs.map +1 -0
- package/dist/cloud/log-streamer.mjs +193 -0
- package/dist/cloud/log-streamer.mjs.map +1 -0
- package/dist/cloud/types.mjs +31 -0
- package/dist/cloud/types.mjs.map +1 -0
- package/dist/config.d.mts +69 -0
- package/dist/config.mjs +13 -0
- package/dist/config.mjs.map +1 -0
- package/dist/define-job.d.mts +34 -0
- package/dist/define-job.mjs +207 -0
- package/dist/define-job.mjs.map +1 -0
- package/dist/discover-jobs.d.mts +14 -0
- package/dist/discover-jobs.mjs +34 -0
- package/dist/discover-jobs.mjs.map +1 -0
- package/dist/help.d.mts +47 -0
- package/dist/help.mjs +173 -0
- package/dist/help.mjs.map +1 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +7 -0
- package/dist/interactive.mjs +191 -0
- package/dist/interactive.mjs.map +1 -0
- package/dist/run-cloud.d.mts +15 -0
- package/dist/run-cloud.mjs +33 -0
- package/dist/run-cloud.mjs.map +1 -0
- package/dist/run-job.d.mts +15 -0
- package/dist/run-job.mjs +125 -0
- package/dist/run-job.mjs.map +1 -0
- package/dist/secrets/loader.mjs +74 -0
- package/dist/secrets/loader.mjs.map +1 -0
- package/dist/types.d.mts +67 -0
- package/package.json +64 -0
- package/src/cli.ts +444 -0
- package/src/cloud/deploy.ts +425 -0
- package/src/cloud/dockerfile.ts +36 -0
- package/src/cloud/execute.ts +145 -0
- package/src/cloud/execution-poller.ts +131 -0
- package/src/cloud/gcloud.ts +164 -0
- package/src/cloud/hash.ts +47 -0
- package/src/cloud/log-streamer.ts +317 -0
- package/src/cloud/run-cloud.ts +29 -0
- package/src/cloud/types.ts +82 -0
- package/src/config.ts +74 -0
- package/src/define-job.test.ts +225 -0
- package/src/define-job.ts +306 -0
- package/src/discover-jobs.test.ts +91 -0
- package/src/discover-jobs.ts +49 -0
- package/src/help.test.ts +155 -0
- package/src/help.ts +291 -0
- package/src/index.ts +25 -0
- package/src/interactive.ts +253 -0
- package/src/run-job.ts +189 -0
- package/src/secrets/index.ts +1 -0
- package/src/secrets/loader.ts +115 -0
- package/src/types.ts +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thijs Koerselman
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { discoverJobs } from "./discover-jobs.mjs";
|
|
3
|
+
import { promptForArgs, selectJob } from "./interactive.mjs";
|
|
4
|
+
import { runJob } from "./run-job.mjs";
|
|
5
|
+
import { deploy, deployIfChanged } from "./cloud/deploy.mjs";
|
|
6
|
+
import { execute } from "./cloud/execute.mjs";
|
|
7
|
+
import { getSecrets } from "./secrets/loader.mjs";
|
|
8
|
+
import { consola } from "consola";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import process from "node:process";
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
|
|
13
|
+
//#region src/cli.ts
|
|
14
|
+
const BIN_NAME = "job";
|
|
15
|
+
const CONFIG_FILE = "job-runner.config.ts";
|
|
16
|
+
const DEFAULT_BUILD_COMMAND = "turbo build";
|
|
17
|
+
const DEFAULT_JOBS_DIRECTORY = "dist/jobs";
|
|
18
|
+
const USAGE = `Usage: ${BIN_NAME} local run <env> <job-name> [options]
|
|
19
|
+
${BIN_NAME} cloud run <env> <job-name> [options]
|
|
20
|
+
${BIN_NAME} cloud deploy <env>
|
|
21
|
+
${BIN_NAME} --list`;
|
|
22
|
+
async function main() {
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
/** Extract flags from anywhere in args */
|
|
25
|
+
const noBuild = args.includes("--no-build");
|
|
26
|
+
const isInteractive = args.includes("--interactive") || args.includes("-i");
|
|
27
|
+
const isAsync = args.includes("--async");
|
|
28
|
+
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
|
|
29
|
+
/** Load config directly (config should only depend on gcp-job-runner) */
|
|
30
|
+
let config;
|
|
31
|
+
try {
|
|
32
|
+
config = (await import(configPath)).default;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
consola.error(`Failed to load runner config from ${configPath}\n${message}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
/** Resolve jobs directory with default */
|
|
39
|
+
const jobsDirectory = config.jobsDirectory ?? path.resolve(process.cwd(), DEFAULT_JOBS_DIRECTORY);
|
|
40
|
+
const envNames = Object.keys(config.environments);
|
|
41
|
+
/** Handle --list: discover and print all available jobs */
|
|
42
|
+
if (args.includes("--list")) {
|
|
43
|
+
if (!noBuild && config.buildCommand !== false) runBuild(config.buildCommand ?? DEFAULT_BUILD_COMMAND);
|
|
44
|
+
const jobs = await discoverJobs(jobsDirectory);
|
|
45
|
+
if (jobs.length === 0) consola.info("No jobs found.");
|
|
46
|
+
else {
|
|
47
|
+
consola.info("Available jobs:");
|
|
48
|
+
for (const job of jobs) consola.log(` ${job.name}`);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
/** Parse positional arguments */
|
|
53
|
+
const positionals = args.filter((arg) => !arg.startsWith("-"));
|
|
54
|
+
const mode = positionals[0];
|
|
55
|
+
if (mode !== "local" && mode !== "cloud") {
|
|
56
|
+
consola.error(`Unknown or missing mode "${mode ?? ""}".\n\n${USAGE}\n\nEnvironments: ${envNames.join(", ")}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const action = positionals[1];
|
|
60
|
+
if (mode === "cloud" && action !== "run" && action !== "deploy") {
|
|
61
|
+
consola.error(`Unknown or missing action "${action ?? ""}" for cloud mode.\n\n` + USAGE);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (mode === "local" && action !== "run") {
|
|
65
|
+
consola.error(`Unknown or missing action "${action ?? ""}" for local mode.\n\n` + USAGE);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const envName = positionals[2];
|
|
69
|
+
if (!envName) {
|
|
70
|
+
consola.error(`No environment specified.\n\n${USAGE}\n\nEnvironments: ${envNames.join(", ")}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
if (!envNames.includes(envName)) {
|
|
74
|
+
consola.error(`Unknown environment "${envName}".\n\nAvailable environments: ${envNames.join(", ")}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
const envConfig = config.environments[envName];
|
|
78
|
+
/**
|
|
79
|
+
* Remaining positionals after env become the job name/path. For example
|
|
80
|
+
* `job cloud run stag test/countdown` -> positionals[3] = "test/countdown".
|
|
81
|
+
*/
|
|
82
|
+
const jobNameFromArgs = positionals[3];
|
|
83
|
+
/**
|
|
84
|
+
* Collect job flags: everything after `<env>` in the original args that
|
|
85
|
+
* isn't a consumed positional or a known global flag.
|
|
86
|
+
*/
|
|
87
|
+
const consumedPositionals = new Set([
|
|
88
|
+
mode,
|
|
89
|
+
action,
|
|
90
|
+
envName,
|
|
91
|
+
jobNameFromArgs
|
|
92
|
+
].filter(Boolean));
|
|
93
|
+
const globalFlags = new Set([
|
|
94
|
+
"--no-build",
|
|
95
|
+
"--interactive",
|
|
96
|
+
"-i",
|
|
97
|
+
"--async",
|
|
98
|
+
"--list"
|
|
99
|
+
]);
|
|
100
|
+
const envIndex = args.indexOf(envName);
|
|
101
|
+
const jobFlags = args.slice(envIndex + 1).filter((arg) => !consumedPositionals.has(arg) && !globalFlags.has(arg));
|
|
102
|
+
/** Build unless skipped */
|
|
103
|
+
if (!noBuild && config.buildCommand !== false) runBuild(config.buildCommand ?? DEFAULT_BUILD_COMMAND);
|
|
104
|
+
if (mode === "local") await handleLocalRun({
|
|
105
|
+
config,
|
|
106
|
+
envName,
|
|
107
|
+
envConfig,
|
|
108
|
+
jobsDirectory,
|
|
109
|
+
jobNameFromArgs,
|
|
110
|
+
jobFlags,
|
|
111
|
+
isInteractive
|
|
112
|
+
});
|
|
113
|
+
else if (action === "deploy") await handleCloudDeploy({
|
|
114
|
+
config,
|
|
115
|
+
envConfig
|
|
116
|
+
});
|
|
117
|
+
else await handleCloudRun({
|
|
118
|
+
config,
|
|
119
|
+
envConfig,
|
|
120
|
+
jobsDirectory,
|
|
121
|
+
jobNameFromArgs,
|
|
122
|
+
jobFlags,
|
|
123
|
+
isInteractive,
|
|
124
|
+
isAsync
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
async function handleLocalRun(options) {
|
|
128
|
+
const { config, envName, envConfig, jobsDirectory, jobNameFromArgs, jobFlags, isInteractive } = options;
|
|
129
|
+
/** Set environment variables for local execution */
|
|
130
|
+
process.env.GOOGLE_CLOUD_PROJECT = envConfig.project;
|
|
131
|
+
process.env.USE_CONSOLE_LOG ??= "true";
|
|
132
|
+
process.env.LOG_COLORIZE ??= "true";
|
|
133
|
+
if (envConfig.env) for (const [key, value] of Object.entries(envConfig.env)) process.env[key] = value;
|
|
134
|
+
if (envConfig.secrets && envConfig.secrets.length > 0) {
|
|
135
|
+
const secrets = await getSecrets(envConfig.secrets);
|
|
136
|
+
for (const [key, value] of Object.entries(secrets)) process.env[key] = value;
|
|
137
|
+
}
|
|
138
|
+
if (isInteractive) {
|
|
139
|
+
const { jobArgv } = await resolveInteractiveJob(jobsDirectory);
|
|
140
|
+
process.argv = [
|
|
141
|
+
process.argv[0],
|
|
142
|
+
process.argv[1],
|
|
143
|
+
...jobArgv
|
|
144
|
+
];
|
|
145
|
+
const commandPrefix = `${BIN_NAME} local run ${envName}`;
|
|
146
|
+
await runJob({
|
|
147
|
+
jobsDirectory,
|
|
148
|
+
initialize: config.initialize,
|
|
149
|
+
logger: config.logger,
|
|
150
|
+
commandPrefix
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (!jobNameFromArgs) {
|
|
155
|
+
consola.error(`No job name specified.\n\nUsage: ${BIN_NAME} local run ${envName} <job-name> [options]\n ${BIN_NAME} local run ${envName} -i`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Rewrite process.argv so runJob sees the job name as the first positional
|
|
160
|
+
* argument, followed by any job-specific flags.
|
|
161
|
+
*/
|
|
162
|
+
process.argv = [
|
|
163
|
+
process.argv[0],
|
|
164
|
+
process.argv[1],
|
|
165
|
+
jobNameFromArgs,
|
|
166
|
+
...jobFlags
|
|
167
|
+
];
|
|
168
|
+
const commandPrefix = `${BIN_NAME} local run ${envName}`;
|
|
169
|
+
await runJob({
|
|
170
|
+
jobsDirectory,
|
|
171
|
+
initialize: config.initialize,
|
|
172
|
+
logger: config.logger,
|
|
173
|
+
commandPrefix
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async function handleCloudDeploy(options) {
|
|
177
|
+
const { config, envConfig } = options;
|
|
178
|
+
const cloud = config.cloud;
|
|
179
|
+
if (!cloud) {
|
|
180
|
+
consola.error("No cloud configuration found in runner config.\nAdd a `cloud` section to your job-runner.config.ts");
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const { imageUri } = await deploy({
|
|
184
|
+
cloud,
|
|
185
|
+
envConfig,
|
|
186
|
+
serviceDirectory: process.cwd()
|
|
187
|
+
});
|
|
188
|
+
consola.info(`Image: ${imageUri}`);
|
|
189
|
+
consola.success("Deploy complete");
|
|
190
|
+
}
|
|
191
|
+
async function handleCloudRun(options) {
|
|
192
|
+
const { config, envConfig, jobsDirectory, jobNameFromArgs, jobFlags, isInteractive, isAsync } = options;
|
|
193
|
+
const cloud = config.cloud;
|
|
194
|
+
if (!cloud) {
|
|
195
|
+
consola.error("No cloud configuration found in runner config.\nAdd a `cloud` section to your job-runner.config.ts");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
const serviceDirectory = process.cwd();
|
|
199
|
+
/** Determine job name and argv */
|
|
200
|
+
let jobArgv;
|
|
201
|
+
if (isInteractive) jobArgv = (await resolveInteractiveJob(jobsDirectory)).jobArgv;
|
|
202
|
+
else {
|
|
203
|
+
if (!jobNameFromArgs) {
|
|
204
|
+
consola.error(`No job name specified.\n\nUsage: ${BIN_NAME} cloud run <env> <job-name> [options]\n ${BIN_NAME} cloud run <env> -i`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
jobArgv = [jobNameFromArgs, ...jobFlags];
|
|
208
|
+
}
|
|
209
|
+
/** Auto-deploy if changed */
|
|
210
|
+
const { imageUri } = await deployIfChanged({
|
|
211
|
+
cloud,
|
|
212
|
+
envConfig,
|
|
213
|
+
serviceDirectory
|
|
214
|
+
});
|
|
215
|
+
consola.info(`Image: ${imageUri}`);
|
|
216
|
+
/** Execute the Cloud Run Job */
|
|
217
|
+
await execute({
|
|
218
|
+
cloud,
|
|
219
|
+
project: envConfig.project,
|
|
220
|
+
jobArgv,
|
|
221
|
+
async: isAsync
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Interactively select a job and prompt for arguments.
|
|
226
|
+
* Returns the job name and the complete jobArgv array.
|
|
227
|
+
*/
|
|
228
|
+
async function resolveInteractiveJob(jobsDirectory) {
|
|
229
|
+
const jobName = await selectJob(jobsDirectory);
|
|
230
|
+
consola.info(`Selected job: ${jobName}`);
|
|
231
|
+
/**
|
|
232
|
+
* Set console-friendly env vars before importing the module, since
|
|
233
|
+
* importing may initialize a structured logger like pino.
|
|
234
|
+
*/
|
|
235
|
+
process.env.USE_CONSOLE_LOG ??= "true";
|
|
236
|
+
process.env.LOG_COLORIZE ??= "true";
|
|
237
|
+
const parts = jobName.split("/");
|
|
238
|
+
const fileName = parts.pop() ?? "";
|
|
239
|
+
const subDirectories = parts;
|
|
240
|
+
const fileLocation = path.join(jobsDirectory, ...subDirectories);
|
|
241
|
+
const modulePath = path.resolve(fileLocation, `${fileName}.mjs`);
|
|
242
|
+
let schema;
|
|
243
|
+
try {
|
|
244
|
+
schema = (await import(modulePath)).default?.__metadata?.schema;
|
|
245
|
+
} catch {}
|
|
246
|
+
/** Prompt for arguments if schema exists */
|
|
247
|
+
let args = {};
|
|
248
|
+
if (schema && Object.keys(schema.shape).length > 0) {
|
|
249
|
+
consola.info("Enter arguments for the job:");
|
|
250
|
+
args = await promptForArgs(schema);
|
|
251
|
+
}
|
|
252
|
+
const jobArgv = [jobName];
|
|
253
|
+
if (Object.keys(args).length > 0) jobArgv.push("--args", JSON.stringify(args));
|
|
254
|
+
return {
|
|
255
|
+
jobName,
|
|
256
|
+
jobArgv
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Run the build command to compile workspace dependencies.
|
|
261
|
+
* Shows a spinner and hides output unless the build fails.
|
|
262
|
+
*/
|
|
263
|
+
function runBuild(command) {
|
|
264
|
+
consola.start("Building jobs source code...");
|
|
265
|
+
try {
|
|
266
|
+
execSync(command, {
|
|
267
|
+
stdio: "pipe",
|
|
268
|
+
encoding: "utf-8"
|
|
269
|
+
});
|
|
270
|
+
consola.success("Build complete");
|
|
271
|
+
} catch (error) {
|
|
272
|
+
consola.fail("Build failed");
|
|
273
|
+
/** Show the build output on failure */
|
|
274
|
+
if (error && typeof error === "object" && "stdout" in error) {
|
|
275
|
+
const stdout = error.stdout;
|
|
276
|
+
if (stdout) consola.log(stdout);
|
|
277
|
+
}
|
|
278
|
+
if (error && typeof error === "object" && "stderr" in error) {
|
|
279
|
+
const stderr = error.stderr;
|
|
280
|
+
if (stderr) consola.log(stderr);
|
|
281
|
+
}
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
await main();
|
|
286
|
+
|
|
287
|
+
//#endregion
|
|
288
|
+
export { };
|
|
289
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { execSync } from \"node:child_process\";\nimport path from \"node:path\";\nimport process from \"node:process\";\nimport { consola } from \"consola\";\nimport type { ZodObject, ZodRawShape } from \"zod\";\nimport type { RunnerConfig } from \"./config\";\nimport { deploy, deployIfChanged } from \"./cloud/deploy\";\nimport { execute } from \"./cloud/execute\";\nimport { discoverJobs } from \"./discover-jobs\";\nimport { promptForArgs, selectJob } from \"./interactive\";\nimport { runJob } from \"./run-job\";\nimport { getSecrets } from \"./secrets\";\nimport type { JobFunction } from \"./types\";\n\nconst BIN_NAME = \"job\";\nconst CONFIG_FILE = \"job-runner.config.ts\";\nconst DEFAULT_BUILD_COMMAND = \"turbo build\";\nconst DEFAULT_JOBS_DIRECTORY = \"dist/jobs\";\n\nconst USAGE = `Usage: ${BIN_NAME} local run <env> <job-name> [options]\n ${BIN_NAME} cloud run <env> <job-name> [options]\n ${BIN_NAME} cloud deploy <env>\n ${BIN_NAME} --list`;\n\nasync function main(): Promise<void> {\n const args = process.argv.slice(2);\n\n /** Extract flags from anywhere in args */\n const noBuild = args.includes(\"--no-build\");\n const isInteractive = args.includes(\"--interactive\") || args.includes(\"-i\");\n const isAsync = args.includes(\"--async\");\n\n const configPath = path.resolve(process.cwd(), CONFIG_FILE);\n\n /** Load config directly (config should only depend on gcp-job-runner) */\n let config: RunnerConfig;\n try {\n const module = (await import(configPath)) as { default: RunnerConfig };\n config = module.default;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n consola.error(\n `Failed to load runner config from ${configPath}\\n${message}`,\n );\n process.exit(1);\n }\n\n /** Resolve jobs directory with default */\n const jobsDirectory =\n config.jobsDirectory ?? path.resolve(process.cwd(), DEFAULT_JOBS_DIRECTORY);\n\n const envNames = Object.keys(config.environments);\n\n /** Handle --list: discover and print all available jobs */\n if (args.includes(\"--list\")) {\n if (!noBuild && config.buildCommand !== false) {\n const buildCommand = config.buildCommand ?? DEFAULT_BUILD_COMMAND;\n runBuild(buildCommand);\n }\n\n const jobs = await discoverJobs(jobsDirectory);\n if (jobs.length === 0) {\n consola.info(\"No jobs found.\");\n } else {\n consola.info(\"Available jobs:\");\n for (const job of jobs) {\n consola.log(` ${job.name}`);\n }\n }\n return;\n }\n\n /** Parse positional arguments */\n const positionals = args.filter((arg) => !arg.startsWith(\"-\"));\n\n const mode = positionals[0];\n if (mode !== \"local\" && mode !== \"cloud\") {\n consola.error(\n `Unknown or missing mode \"${mode ?? \"\"}\".\\n\\n` +\n `${USAGE}\\n\\n` +\n `Environments: ${envNames.join(\", \")}`,\n );\n process.exit(1);\n }\n\n const action = positionals[1];\n if (mode === \"cloud\" && action !== \"run\" && action !== \"deploy\") {\n consola.error(\n `Unknown or missing action \"${action ?? \"\"}\" for cloud mode.\\n\\n` + USAGE,\n );\n process.exit(1);\n }\n if (mode === \"local\" && action !== \"run\") {\n consola.error(\n `Unknown or missing action \"${action ?? \"\"}\" for local mode.\\n\\n` + USAGE,\n );\n process.exit(1);\n }\n\n const envName = positionals[2];\n if (!envName) {\n consola.error(\n `No environment specified.\\n\\n` +\n `${USAGE}\\n\\n` +\n `Environments: ${envNames.join(\", \")}`,\n );\n process.exit(1);\n }\n\n if (!envNames.includes(envName)) {\n consola.error(\n `Unknown environment \"${envName}\".\\n\\n` +\n `Available environments: ${envNames.join(\", \")}`,\n );\n process.exit(1);\n }\n\n const envConfig = config.environments[envName]!;\n\n /**\n * Remaining positionals after env become the job name/path. For example\n * `job cloud run stag test/countdown` -> positionals[3] = \"test/countdown\".\n */\n const jobNameFromArgs = positionals[3];\n\n /**\n * Collect job flags: everything after `<env>` in the original args that\n * isn't a consumed positional or a known global flag.\n */\n const consumedPositionals = new Set(\n [mode, action, envName, jobNameFromArgs].filter(Boolean),\n );\n const globalFlags = new Set([\n \"--no-build\",\n \"--interactive\",\n \"-i\",\n \"--async\",\n \"--list\",\n ]);\n\n const envIndex = args.indexOf(envName);\n const jobFlags = args\n .slice(envIndex + 1)\n .filter((arg) => !consumedPositionals.has(arg) && !globalFlags.has(arg));\n\n /** Build unless skipped */\n if (!noBuild && config.buildCommand !== false) {\n const buildCommand = config.buildCommand ?? DEFAULT_BUILD_COMMAND;\n runBuild(buildCommand);\n }\n\n if (mode === \"local\") {\n await handleLocalRun({\n config,\n envName,\n envConfig,\n jobsDirectory,\n jobNameFromArgs,\n jobFlags,\n isInteractive,\n });\n } else if (action === \"deploy\") {\n await handleCloudDeploy({ config, envConfig });\n } else {\n await handleCloudRun({\n config,\n envConfig,\n jobsDirectory,\n jobNameFromArgs,\n jobFlags,\n isInteractive,\n isAsync,\n });\n }\n}\n\ninterface LocalRunOptions {\n config: RunnerConfig;\n envName: string;\n envConfig: RunnerConfig[\"environments\"][string];\n jobsDirectory: string;\n jobNameFromArgs: string | undefined;\n jobFlags: string[];\n isInteractive: boolean;\n}\n\nasync function handleLocalRun(options: LocalRunOptions): Promise<void> {\n const {\n config,\n envName,\n envConfig,\n jobsDirectory,\n jobNameFromArgs,\n jobFlags,\n isInteractive,\n } = options;\n\n /** Set environment variables for local execution */\n process.env.GOOGLE_CLOUD_PROJECT = envConfig.project;\n process.env.USE_CONSOLE_LOG ??= \"true\";\n process.env.LOG_COLORIZE ??= \"true\";\n\n if (envConfig.env) {\n for (const [key, value] of Object.entries(envConfig.env)) {\n process.env[key] = value;\n }\n }\n\n if (envConfig.secrets && envConfig.secrets.length > 0) {\n const secrets = await getSecrets(envConfig.secrets);\n for (const [key, value] of Object.entries(secrets)) {\n process.env[key] = value;\n }\n }\n\n if (isInteractive) {\n const { jobArgv } = await resolveInteractiveJob(jobsDirectory);\n\n process.argv = [process.argv[0]!, process.argv[1]!, ...jobArgv];\n\n const commandPrefix = `${BIN_NAME} local run ${envName}`;\n\n await runJob({\n jobsDirectory,\n initialize: config.initialize,\n logger: config.logger,\n commandPrefix,\n });\n return;\n }\n\n if (!jobNameFromArgs) {\n consola.error(\n `No job name specified.\\n\\n` +\n `Usage: ${BIN_NAME} local run ${envName} <job-name> [options]\\n` +\n ` ${BIN_NAME} local run ${envName} -i`,\n );\n process.exit(1);\n }\n\n /**\n * Rewrite process.argv so runJob sees the job name as the first positional\n * argument, followed by any job-specific flags.\n */\n process.argv = [\n process.argv[0]!,\n process.argv[1]!,\n jobNameFromArgs,\n ...jobFlags,\n ];\n\n const commandPrefix = `${BIN_NAME} local run ${envName}`;\n\n await runJob({\n jobsDirectory,\n initialize: config.initialize,\n logger: config.logger,\n commandPrefix,\n });\n}\n\ninterface CloudDeployOptions {\n config: RunnerConfig;\n envConfig: RunnerConfig[\"environments\"][string];\n}\n\nasync function handleCloudDeploy(options: CloudDeployOptions): Promise<void> {\n const { config, envConfig } = options;\n const cloud = config.cloud;\n\n if (!cloud) {\n consola.error(\n \"No cloud configuration found in runner config.\\n\" +\n \"Add a `cloud` section to your job-runner.config.ts\",\n );\n process.exit(1);\n }\n\n const serviceDirectory = process.cwd();\n\n const { imageUri } = await deploy({\n cloud,\n envConfig,\n serviceDirectory,\n });\n\n consola.info(`Image: ${imageUri}`);\n consola.success(\"Deploy complete\");\n}\n\ninterface CloudRunOptions {\n config: RunnerConfig;\n envConfig: RunnerConfig[\"environments\"][string];\n jobsDirectory: string;\n jobNameFromArgs: string | undefined;\n jobFlags: string[];\n isInteractive: boolean;\n isAsync: boolean;\n}\n\nasync function handleCloudRun(options: CloudRunOptions): Promise<void> {\n const {\n config,\n envConfig,\n jobsDirectory,\n jobNameFromArgs,\n jobFlags,\n isInteractive,\n isAsync,\n } = options;\n\n const cloud = config.cloud;\n\n if (!cloud) {\n consola.error(\n \"No cloud configuration found in runner config.\\n\" +\n \"Add a `cloud` section to your job-runner.config.ts\",\n );\n process.exit(1);\n }\n\n const serviceDirectory = process.cwd();\n\n /** Determine job name and argv */\n let jobArgv: string[];\n\n if (isInteractive) {\n const result = await resolveInteractiveJob(jobsDirectory);\n jobArgv = result.jobArgv;\n } else {\n if (!jobNameFromArgs) {\n consola.error(\n `No job name specified.\\n\\n` +\n `Usage: ${BIN_NAME} cloud run <env> <job-name> [options]\\n` +\n ` ${BIN_NAME} cloud run <env> -i`,\n );\n process.exit(1);\n }\n jobArgv = [jobNameFromArgs, ...jobFlags];\n }\n\n /** Auto-deploy if changed */\n const { imageUri } = await deployIfChanged({\n cloud,\n envConfig,\n serviceDirectory,\n });\n\n consola.info(`Image: ${imageUri}`);\n\n /** Execute the Cloud Run Job */\n await execute({\n cloud,\n project: envConfig.project,\n jobArgv,\n async: isAsync,\n });\n}\n\n/**\n * Interactively select a job and prompt for arguments.\n * Returns the job name and the complete jobArgv array.\n */\nasync function resolveInteractiveJob(\n jobsDirectory: string,\n): Promise<{ jobName: string; jobArgv: string[] }> {\n const jobName = await selectJob(jobsDirectory);\n\n consola.info(`Selected job: ${jobName}`);\n\n /**\n * Set console-friendly env vars before importing the module, since\n * importing may initialize a structured logger like pino.\n */\n process.env.USE_CONSOLE_LOG ??= \"true\";\n process.env.LOG_COLORIZE ??= \"true\";\n\n const parts = jobName.split(\"/\");\n const fileName = parts.pop() ?? \"\";\n const subDirectories = parts;\n const fileLocation = path.join(jobsDirectory, ...subDirectories);\n const modulePath = path.resolve(fileLocation, `${fileName}.mjs`);\n\n let schema: ZodObject<ZodRawShape> | undefined;\n try {\n const moduleObject = (await import(modulePath)) as Record<string, unknown>;\n const fn = moduleObject.default as JobFunction | undefined;\n schema = fn?.__metadata?.schema;\n } catch {\n /** Module might not exist yet or have errors - proceed without schema */\n }\n\n /** Prompt for arguments if schema exists */\n let args: Record<string, unknown> = {};\n if (schema && Object.keys(schema.shape).length > 0) {\n consola.info(\"Enter arguments for the job:\");\n args = await promptForArgs(schema);\n }\n\n const jobArgv = [jobName];\n if (Object.keys(args).length > 0) {\n jobArgv.push(\"--args\", JSON.stringify(args));\n }\n\n return { jobName, jobArgv };\n}\n\n/**\n * Run the build command to compile workspace dependencies.\n * Shows a spinner and hides output unless the build fails.\n */\nfunction runBuild(command: string): void {\n consola.start(\"Building jobs source code...\");\n\n try {\n execSync(command, {\n stdio: \"pipe\",\n encoding: \"utf-8\",\n });\n consola.success(\"Build complete\");\n } catch (error) {\n consola.fail(\"Build failed\");\n\n /** Show the build output on failure */\n if (error && typeof error === \"object\" && \"stdout\" in error) {\n const stdout = (error as { stdout?: string }).stdout;\n if (stdout) {\n consola.log(stdout);\n }\n }\n if (error && typeof error === \"object\" && \"stderr\" in error) {\n const stderr = (error as { stderr?: string }).stderr;\n if (stderr) {\n consola.log(stderr);\n }\n }\n\n process.exit(1);\n }\n}\n\nawait main();\n"],"mappings":"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"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { generateDockerfile } from "./dockerfile.mjs";
|
|
2
|
+
import { checkGcloudAvailable, gcloudExec, gcloudExecCapture, gcloudJson, isDockerAvailable, shellExecCapture } from "./gcloud.mjs";
|
|
3
|
+
import { hashDirectory } from "./hash.mjs";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
//#region src/cloud/deploy.ts
|
|
9
|
+
const DEFAULT_REGION = "us-central1";
|
|
10
|
+
const DEFAULT_ARTIFACT_REGISTRY = "cloud-run";
|
|
11
|
+
const DEFAULT_ISOLATE_PATH = "isolate";
|
|
12
|
+
const GENERATED_DOCKERFILE = "Dockerfile";
|
|
13
|
+
const REGISTRY = "docker.pkg.dev";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the isolate output directory path.
|
|
16
|
+
* Reads isolate.config.json if present, otherwise uses default "./isolate".
|
|
17
|
+
*/
|
|
18
|
+
function resolveIsolateDirectory(serviceDirectory) {
|
|
19
|
+
const configPath = path.join(serviceDirectory, "isolate.config.json");
|
|
20
|
+
if (existsSync(configPath)) try {
|
|
21
|
+
const configContent = readFileSync(configPath, "utf-8");
|
|
22
|
+
const config = JSON.parse(configContent);
|
|
23
|
+
if (config.targetPackagePath) return path.join(serviceDirectory, config.targetPackagePath);
|
|
24
|
+
} catch {}
|
|
25
|
+
return path.join(serviceDirectory, DEFAULT_ISOLATE_PATH);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shared preparation logic: isolate, hash, check image, build if needed.
|
|
29
|
+
* Used by both deploy() and deployIfChanged().
|
|
30
|
+
*/
|
|
31
|
+
async function prepareImage(options) {
|
|
32
|
+
const { cloud, envConfig, serviceDirectory } = options;
|
|
33
|
+
const region = cloud.region ?? DEFAULT_REGION;
|
|
34
|
+
const artifactRegistry = cloud.artifactRegistry ?? DEFAULT_ARTIFACT_REGISTRY;
|
|
35
|
+
const project = envConfig.project;
|
|
36
|
+
let buildLocal = cloud.buildLocal !== false;
|
|
37
|
+
checkGcloudAvailable();
|
|
38
|
+
if (buildLocal && !isDockerAvailable()) {
|
|
39
|
+
consola.warn("Docker is not available, falling back to Cloud Build. Install Docker for faster local builds: https://docs.docker.com/get-docker/");
|
|
40
|
+
buildLocal = false;
|
|
41
|
+
}
|
|
42
|
+
/** Step 1: Run isolate to bundle workspace dependencies */
|
|
43
|
+
const isolateDirectory = resolveIsolateDirectory(serviceDirectory);
|
|
44
|
+
consola.start("Isolating package...");
|
|
45
|
+
try {
|
|
46
|
+
const { isolate: runIsolate } = await import("isolate-package");
|
|
47
|
+
const configPath = path.join(serviceDirectory, "isolate.config.json");
|
|
48
|
+
await runIsolate({
|
|
49
|
+
...existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : {},
|
|
50
|
+
includeDevDependencies: true
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
consola.error(`Failed to isolate package: ${message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
consola.success("Package isolated");
|
|
58
|
+
/** Step 2: Hash the isolate directory */
|
|
59
|
+
const tag = await hashDirectory(isolateDirectory);
|
|
60
|
+
consola.info(`Content hash: ${tag}`);
|
|
61
|
+
/** Step 3: Check if image already exists */
|
|
62
|
+
const imageUri = `${region}-${REGISTRY}/${project}/${artifactRegistry}/${cloud.name}:${tag}`;
|
|
63
|
+
const imageExists = checkImageExists(imageUri, project);
|
|
64
|
+
let imageBuilt = false;
|
|
65
|
+
if (imageExists) consola.success(`Image already exists: ${cloud.name}:${tag}`);
|
|
66
|
+
else if (buildLocal) {
|
|
67
|
+
/** Step 4a: Generate Dockerfile and build locally with Docker */
|
|
68
|
+
consola.start("Building image locally with Docker...");
|
|
69
|
+
const dockerfilePath = path.join(serviceDirectory, GENERATED_DOCKERFILE);
|
|
70
|
+
writeFileSync(dockerfilePath, generateDockerfile());
|
|
71
|
+
try {
|
|
72
|
+
const buildResult = shellExecCapture(`docker build --platform linux/amd64 -t ${imageUri} .`, { cwd: serviceDirectory });
|
|
73
|
+
if (!buildResult.success) {
|
|
74
|
+
consola.error("Docker build failed. Output:\n" + buildResult.output);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
consola.success(`Image built: ${cloud.name}:${tag}`);
|
|
78
|
+
/** Configure Docker authentication for Artifact Registry */
|
|
79
|
+
gcloudExecCapture([
|
|
80
|
+
"auth",
|
|
81
|
+
"configure-docker",
|
|
82
|
+
`${region}-${REGISTRY}`,
|
|
83
|
+
"--quiet"
|
|
84
|
+
]);
|
|
85
|
+
consola.start("Pushing image to Artifact Registry...");
|
|
86
|
+
const pushResult = shellExecCapture(`docker push ${imageUri}`);
|
|
87
|
+
if (!pushResult.success) {
|
|
88
|
+
consola.error("Docker push failed. Output:\n" + pushResult.output);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
consola.success(`Image pushed: ${cloud.name}:${tag}`);
|
|
92
|
+
imageBuilt = true;
|
|
93
|
+
} finally {
|
|
94
|
+
/** Clean up generated Dockerfile */
|
|
95
|
+
try {
|
|
96
|
+
unlinkSync(dockerfilePath);
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
/** Step 4b: Generate Dockerfile and build with Cloud Build */
|
|
101
|
+
consola.start("Building image with Cloud Build...");
|
|
102
|
+
const dockerfilePath = path.join(serviceDirectory, GENERATED_DOCKERFILE);
|
|
103
|
+
writeFileSync(dockerfilePath, generateDockerfile());
|
|
104
|
+
try {
|
|
105
|
+
const buildResult = gcloudExecCapture([
|
|
106
|
+
"builds",
|
|
107
|
+
"submit",
|
|
108
|
+
"--project",
|
|
109
|
+
project,
|
|
110
|
+
"--region",
|
|
111
|
+
region,
|
|
112
|
+
`--tag=${imageUri}`,
|
|
113
|
+
"."
|
|
114
|
+
], { cwd: serviceDirectory });
|
|
115
|
+
/** Extract and show the Cloud Build logs URL if available */
|
|
116
|
+
const logsUrlMatch = buildResult.output.match(/Logs are available at \[(.+?)]/);
|
|
117
|
+
if (logsUrlMatch) consola.info(`Cloud Build logs: ${logsUrlMatch[1]}`);
|
|
118
|
+
if (!buildResult.success) {
|
|
119
|
+
consola.error("Cloud Build failed. Output:\n" + buildResult.output);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
consola.success(`Image built: ${cloud.name}:${tag}`);
|
|
123
|
+
imageBuilt = true;
|
|
124
|
+
} finally {
|
|
125
|
+
/** Clean up generated Dockerfile */
|
|
126
|
+
try {
|
|
127
|
+
unlinkSync(dockerfilePath);
|
|
128
|
+
} catch {}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
imageUri,
|
|
133
|
+
imageBuilt,
|
|
134
|
+
region,
|
|
135
|
+
project
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Build and deploy a Cloud Run Job image.
|
|
140
|
+
*
|
|
141
|
+
* Always creates or updates the Cloud Run Job resource, regardless of whether
|
|
142
|
+
* the image changed. Use deployIfChanged() to skip when unchanged.
|
|
143
|
+
*/
|
|
144
|
+
async function deploy(options) {
|
|
145
|
+
const { imageUri, imageBuilt, region, project } = await prepareImage(options);
|
|
146
|
+
return {
|
|
147
|
+
imageUri,
|
|
148
|
+
imageBuilt,
|
|
149
|
+
jobCreated: await createOrUpdateJob({
|
|
150
|
+
cloud: options.cloud,
|
|
151
|
+
envConfig: options.envConfig,
|
|
152
|
+
imageUri,
|
|
153
|
+
region,
|
|
154
|
+
project
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Build and conditionally deploy a Cloud Run Job image.
|
|
160
|
+
*
|
|
161
|
+
* Only creates/updates the Cloud Run Job when the image has changed.
|
|
162
|
+
* When the image already exists, skips both the Docker build and the
|
|
163
|
+
* createOrUpdateJob step.
|
|
164
|
+
*/
|
|
165
|
+
async function deployIfChanged(options) {
|
|
166
|
+
const { imageUri, imageBuilt, region, project } = await prepareImage(options);
|
|
167
|
+
if (!imageBuilt) {
|
|
168
|
+
consola.info("No changes detected, skipping deploy");
|
|
169
|
+
return {
|
|
170
|
+
imageUri,
|
|
171
|
+
imageBuilt: false,
|
|
172
|
+
jobCreated: false
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
consola.info("Changes detected, deploying...");
|
|
176
|
+
return {
|
|
177
|
+
imageUri,
|
|
178
|
+
imageBuilt,
|
|
179
|
+
jobCreated: await createOrUpdateJob({
|
|
180
|
+
cloud: options.cloud,
|
|
181
|
+
envConfig: options.envConfig,
|
|
182
|
+
imageUri,
|
|
183
|
+
region,
|
|
184
|
+
project
|
|
185
|
+
})
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create or update a Cloud Run Job resource.
|
|
190
|
+
* Returns true if the job was newly created, false if updated.
|
|
191
|
+
*/
|
|
192
|
+
async function createOrUpdateJob(options) {
|
|
193
|
+
const { cloud, envConfig, imageUri, region, project } = options;
|
|
194
|
+
const memory = cloud.resources?.memory ?? "512Mi";
|
|
195
|
+
const cpu = cloud.resources?.cpu ?? "1";
|
|
196
|
+
const timeout = cloud.resources?.timeout ?? 3600;
|
|
197
|
+
/** Check if job already exists */
|
|
198
|
+
const existingJob = gcloudJson([
|
|
199
|
+
"run",
|
|
200
|
+
"jobs",
|
|
201
|
+
"describe",
|
|
202
|
+
cloud.name,
|
|
203
|
+
"--project",
|
|
204
|
+
project,
|
|
205
|
+
"--region",
|
|
206
|
+
region
|
|
207
|
+
], { ignoreErrors: true });
|
|
208
|
+
/** Build environment variables */
|
|
209
|
+
const envVars = {
|
|
210
|
+
GOOGLE_CLOUD_PROJECT: project,
|
|
211
|
+
...envConfig.env
|
|
212
|
+
};
|
|
213
|
+
const envVarsString = Object.entries(envVars).map(([key, value]) => `${key}=${value}`).join(",");
|
|
214
|
+
const secretsString = (envConfig.secrets ?? []).map((name) => `${name}=${name}:latest`).join(",");
|
|
215
|
+
if (existingJob) {
|
|
216
|
+
consola.start("Updating Cloud Run Job...");
|
|
217
|
+
const updateArgs = [
|
|
218
|
+
"run",
|
|
219
|
+
"jobs",
|
|
220
|
+
"update",
|
|
221
|
+
cloud.name,
|
|
222
|
+
"--project",
|
|
223
|
+
project,
|
|
224
|
+
"--region",
|
|
225
|
+
region,
|
|
226
|
+
`--image=${imageUri}`,
|
|
227
|
+
`--set-env-vars=${envVarsString}`,
|
|
228
|
+
`--memory=${memory}`,
|
|
229
|
+
`--cpu=${cpu}`,
|
|
230
|
+
`--task-timeout=${timeout}s`,
|
|
231
|
+
"--max-retries=0"
|
|
232
|
+
];
|
|
233
|
+
if (secretsString) updateArgs.push(`--set-secrets=${secretsString}`);
|
|
234
|
+
if (cloud.serviceAccount) updateArgs.push(`--service-account=${cloud.serviceAccount}`);
|
|
235
|
+
if (!gcloudExec(updateArgs)) {
|
|
236
|
+
consola.error("Failed to update Cloud Run Job");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
consola.success(`Cloud Run Job updated: ${cloud.name}`);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
consola.start("Creating Cloud Run Job...");
|
|
243
|
+
const createArgs = [
|
|
244
|
+
"run",
|
|
245
|
+
"jobs",
|
|
246
|
+
"create",
|
|
247
|
+
cloud.name,
|
|
248
|
+
"--project",
|
|
249
|
+
project,
|
|
250
|
+
"--region",
|
|
251
|
+
region,
|
|
252
|
+
`--image=${imageUri}`,
|
|
253
|
+
`--set-env-vars=${envVarsString}`,
|
|
254
|
+
`--memory=${memory}`,
|
|
255
|
+
`--cpu=${cpu}`,
|
|
256
|
+
`--task-timeout=${timeout}s`,
|
|
257
|
+
"--max-retries=0"
|
|
258
|
+
];
|
|
259
|
+
if (secretsString) createArgs.push(`--set-secrets=${secretsString}`);
|
|
260
|
+
if (cloud.serviceAccount) createArgs.push(`--service-account=${cloud.serviceAccount}`);
|
|
261
|
+
if (!gcloudExec(createArgs)) {
|
|
262
|
+
consola.error("Failed to create Cloud Run Job");
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
consola.success(`Cloud Run Job created: ${cloud.name}`);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Check if a Docker image exists in Artifact Registry.
|
|
270
|
+
*/
|
|
271
|
+
function checkImageExists(imageUri, project) {
|
|
272
|
+
return gcloudJson([
|
|
273
|
+
"artifacts",
|
|
274
|
+
"docker",
|
|
275
|
+
"images",
|
|
276
|
+
"describe",
|
|
277
|
+
imageUri,
|
|
278
|
+
"--project",
|
|
279
|
+
project
|
|
280
|
+
], { ignoreErrors: true }) !== void 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
//#endregion
|
|
284
|
+
export { deploy, deployIfChanged };
|
|
285
|
+
//# sourceMappingURL=deploy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deploy.mjs","names":[],"sources":["../../src/cloud/deploy.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync, unlinkSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { consola } from \"consola\";\nimport type { CloudConfig, RunnerEnvOptions } from \"../config\";\nimport { generateDockerfile } from \"./dockerfile\";\nimport {\n checkGcloudAvailable,\n isDockerAvailable,\n gcloudExec,\n gcloudExecCapture,\n gcloudJson,\n shellExecCapture,\n} from \"./gcloud\";\nimport { hashDirectory } from \"./hash\";\n\nexport interface DeployOptions {\n /** Cloud configuration from the runner config */\n cloud: CloudConfig;\n /** Environment configuration (project, env vars, secrets) */\n envConfig: RunnerEnvOptions;\n /** Working directory (service root where isolate.config.json lives) */\n serviceDirectory: string;\n}\n\nexport interface DeployResult {\n /** Full image URI including tag */\n imageUri: string;\n /** Whether a new image was built */\n imageBuilt: boolean;\n /** Whether the Cloud Run Job was created or updated */\n jobCreated: boolean;\n}\n\nconst DEFAULT_REGION = \"us-central1\";\nconst DEFAULT_ARTIFACT_REGISTRY = \"cloud-run\";\nconst DEFAULT_ISOLATE_PATH = \"isolate\";\nconst GENERATED_DOCKERFILE = \"Dockerfile\";\nconst REGISTRY = \"docker.pkg.dev\";\n\ninterface IsolateConfig {\n targetPackagePath?: string;\n}\n\n/**\n * Resolve the isolate output directory path.\n * Reads isolate.config.json if present, otherwise uses default \"./isolate\".\n */\nfunction resolveIsolateDirectory(serviceDirectory: string): string {\n const configPath = path.join(serviceDirectory, \"isolate.config.json\");\n\n if (existsSync(configPath)) {\n try {\n const configContent = readFileSync(configPath, \"utf-8\");\n const config = JSON.parse(configContent) as IsolateConfig;\n if (config.targetPackagePath) {\n return path.join(serviceDirectory, config.targetPackagePath);\n }\n } catch {\n /** Ignore parse errors, use default */\n }\n }\n\n return path.join(serviceDirectory, DEFAULT_ISOLATE_PATH);\n}\n\ninterface PrepareResult {\n imageUri: string;\n imageBuilt: boolean;\n region: string;\n project: string;\n}\n\n/**\n * Shared preparation logic: isolate, hash, check image, build if needed.\n * Used by both deploy() and deployIfChanged().\n */\nasync function prepareImage(options: DeployOptions): Promise<PrepareResult> {\n const { cloud, envConfig, serviceDirectory } = options;\n const region = cloud.region ?? DEFAULT_REGION;\n const artifactRegistry = cloud.artifactRegistry ?? DEFAULT_ARTIFACT_REGISTRY;\n const project = envConfig.project;\n let buildLocal = cloud.buildLocal !== false;\n\n checkGcloudAvailable();\n\n if (buildLocal && !isDockerAvailable()) {\n consola.warn(\n \"Docker is not available, falling back to Cloud Build. \" +\n \"Install Docker for faster local builds: https://docs.docker.com/get-docker/\",\n );\n buildLocal = false;\n }\n\n /** Step 1: Run isolate to bundle workspace dependencies */\n const isolateDirectory = resolveIsolateDirectory(serviceDirectory);\n\n consola.start(\"Isolating package...\");\n\n try {\n const { isolate: runIsolate } = await import(\"isolate-package\");\n\n const configPath = path.join(serviceDirectory, \"isolate.config.json\");\n const fileConfig = existsSync(configPath)\n ? JSON.parse(readFileSync(configPath, \"utf-8\"))\n : {};\n\n await runIsolate({\n ...fileConfig,\n includeDevDependencies: true,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n consola.error(`Failed to isolate package: ${message}`);\n process.exit(1);\n }\n\n consola.success(\"Package isolated\");\n\n /** Step 2: Hash the isolate directory */\n const tag = await hashDirectory(isolateDirectory);\n consola.info(`Content hash: ${tag}`);\n\n /** Step 3: Check if image already exists */\n const imageUri = `${region}-${REGISTRY}/${project}/${artifactRegistry}/${cloud.name}:${tag}`;\n\n const imageExists = checkImageExists(imageUri, project);\n let imageBuilt = false;\n\n if (imageExists) {\n consola.success(`Image already exists: ${cloud.name}:${tag}`);\n } else if (buildLocal) {\n /** Step 4a: Generate Dockerfile and build locally with Docker */\n consola.start(\"Building image locally with Docker...\");\n\n const dockerfilePath = path.join(serviceDirectory, GENERATED_DOCKERFILE);\n writeFileSync(dockerfilePath, generateDockerfile());\n\n try {\n const buildResult = shellExecCapture(\n `docker build --platform linux/amd64 -t ${imageUri} .`,\n { cwd: serviceDirectory },\n );\n\n if (!buildResult.success) {\n consola.error(\"Docker build failed. Output:\\n\" + buildResult.output);\n process.exit(1);\n }\n\n consola.success(`Image built: ${cloud.name}:${tag}`);\n\n /** Configure Docker authentication for Artifact Registry */\n gcloudExecCapture([\n \"auth\",\n \"configure-docker\",\n `${region}-${REGISTRY}`,\n \"--quiet\",\n ]);\n\n consola.start(\"Pushing image to Artifact Registry...\");\n\n const pushResult = shellExecCapture(`docker push ${imageUri}`);\n\n if (!pushResult.success) {\n consola.error(\"Docker push failed. Output:\\n\" + pushResult.output);\n process.exit(1);\n }\n\n consola.success(`Image pushed: ${cloud.name}:${tag}`);\n imageBuilt = true;\n } finally {\n /** Clean up generated Dockerfile */\n try {\n unlinkSync(dockerfilePath);\n } catch {\n /** Ignore cleanup errors */\n }\n }\n } else {\n /** Step 4b: Generate Dockerfile and build with Cloud Build */\n consola.start(\"Building image with Cloud Build...\");\n\n const dockerfilePath = path.join(serviceDirectory, GENERATED_DOCKERFILE);\n writeFileSync(dockerfilePath, generateDockerfile());\n\n try {\n const buildResult = gcloudExecCapture(\n [\n \"builds\",\n \"submit\",\n \"--project\",\n project,\n \"--region\",\n region,\n `--tag=${imageUri}`,\n \".\",\n ],\n { cwd: serviceDirectory },\n );\n\n /** Extract and show the Cloud Build logs URL if available */\n const logsUrlMatch = buildResult.output.match(\n /Logs are available at \\[(.+?)]/,\n );\n if (logsUrlMatch) {\n consola.info(`Cloud Build logs: ${logsUrlMatch[1]}`);\n }\n\n if (!buildResult.success) {\n consola.error(\"Cloud Build failed. Output:\\n\" + buildResult.output);\n process.exit(1);\n }\n\n consola.success(`Image built: ${cloud.name}:${tag}`);\n imageBuilt = true;\n } finally {\n /** Clean up generated Dockerfile */\n try {\n unlinkSync(dockerfilePath);\n } catch {\n /** Ignore cleanup errors */\n }\n }\n }\n\n return { imageUri, imageBuilt, region, project };\n}\n\n/**\n * Build and deploy a Cloud Run Job image.\n *\n * Always creates or updates the Cloud Run Job resource, regardless of whether\n * the image changed. Use deployIfChanged() to skip when unchanged.\n */\nexport async function deploy(options: DeployOptions): Promise<DeployResult> {\n const { imageUri, imageBuilt, region, project } = await prepareImage(options);\n\n /** Always create or update the Cloud Run Job */\n const jobCreated = await createOrUpdateJob({\n cloud: options.cloud,\n envConfig: options.envConfig,\n imageUri,\n region,\n project,\n });\n\n return { imageUri, imageBuilt, jobCreated };\n}\n\n/**\n * Build and conditionally deploy a Cloud Run Job image.\n *\n * Only creates/updates the Cloud Run Job when the image has changed.\n * When the image already exists, skips both the Docker build and the\n * createOrUpdateJob step.\n */\nexport async function deployIfChanged(\n options: DeployOptions,\n): Promise<DeployResult> {\n const { imageUri, imageBuilt, region, project } = await prepareImage(options);\n\n if (!imageBuilt) {\n consola.info(\"No changes detected, skipping deploy\");\n return { imageUri, imageBuilt: false, jobCreated: false };\n }\n\n consola.info(\"Changes detected, deploying...\");\n\n const jobCreated = await createOrUpdateJob({\n cloud: options.cloud,\n envConfig: options.envConfig,\n imageUri,\n region,\n project,\n });\n\n return { imageUri, imageBuilt, jobCreated };\n}\n\ninterface CreateOrUpdateJobOptions {\n cloud: CloudConfig;\n envConfig: RunnerEnvOptions;\n imageUri: string;\n region: string;\n project: string;\n}\n\n/**\n * Create or update a Cloud Run Job resource.\n * Returns true if the job was newly created, false if updated.\n */\nasync function createOrUpdateJob(\n options: CreateOrUpdateJobOptions,\n): Promise<boolean> {\n const { cloud, envConfig, imageUri, region, project } = options;\n const memory = cloud.resources?.memory ?? \"512Mi\";\n const cpu = cloud.resources?.cpu ?? \"1\";\n const timeout = cloud.resources?.timeout ?? 3600;\n\n /** Check if job already exists */\n const existingJob = gcloudJson(\n [\n \"run\",\n \"jobs\",\n \"describe\",\n cloud.name,\n \"--project\",\n project,\n \"--region\",\n region,\n ],\n { ignoreErrors: true },\n );\n\n /** Build environment variables */\n const envVars: Record<string, string> = {\n GOOGLE_CLOUD_PROJECT: project,\n ...envConfig.env,\n };\n\n const envVarsString = Object.entries(envVars)\n .map(([key, value]) => `${key}=${value}`)\n .join(\",\");\n\n /** Build secret references */\n const secretNames = envConfig.secrets ?? [];\n const secretsString = secretNames\n .map((name) => `${name}=${name}:latest`)\n .join(\",\");\n\n if (existingJob) {\n consola.start(\"Updating Cloud Run Job...\");\n\n const updateArgs = [\n \"run\",\n \"jobs\",\n \"update\",\n cloud.name,\n \"--project\",\n project,\n \"--region\",\n region,\n `--image=${imageUri}`,\n `--set-env-vars=${envVarsString}`,\n `--memory=${memory}`,\n `--cpu=${cpu}`,\n `--task-timeout=${timeout}s`,\n \"--max-retries=0\",\n ];\n\n if (secretsString) {\n updateArgs.push(`--set-secrets=${secretsString}`);\n }\n\n if (cloud.serviceAccount) {\n updateArgs.push(`--service-account=${cloud.serviceAccount}`);\n }\n\n const success = gcloudExec(updateArgs);\n\n if (!success) {\n consola.error(\"Failed to update Cloud Run Job\");\n process.exit(1);\n }\n\n consola.success(`Cloud Run Job updated: ${cloud.name}`);\n return false;\n }\n\n consola.start(\"Creating Cloud Run Job...\");\n\n const createArgs = [\n \"run\",\n \"jobs\",\n \"create\",\n cloud.name,\n \"--project\",\n project,\n \"--region\",\n region,\n `--image=${imageUri}`,\n `--set-env-vars=${envVarsString}`,\n `--memory=${memory}`,\n `--cpu=${cpu}`,\n `--task-timeout=${timeout}s`,\n \"--max-retries=0\",\n ];\n\n if (secretsString) {\n createArgs.push(`--set-secrets=${secretsString}`);\n }\n\n if (cloud.serviceAccount) {\n createArgs.push(`--service-account=${cloud.serviceAccount}`);\n }\n\n const success = gcloudExec(createArgs);\n\n if (!success) {\n consola.error(\"Failed to create Cloud Run Job\");\n process.exit(1);\n }\n\n consola.success(`Cloud Run Job created: ${cloud.name}`);\n return true;\n}\n\n/**\n * Check if a Docker image exists in Artifact Registry.\n */\nfunction checkImageExists(imageUri: string, project: string): boolean {\n const result = gcloudJson(\n [\n \"artifacts\",\n \"docker\",\n \"images\",\n \"describe\",\n imageUri,\n \"--project\",\n project,\n ],\n { ignoreErrors: true },\n );\n\n return result !== undefined;\n}\n"],"mappings":"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"}
|