hammerkit 1.27.0-next.27 → 1.27.0-next.28

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/main.js CHANGED
@@ -8,6 +8,7 @@ const redact_1 = require("./utils/redact");
8
8
  const empty_writable_1 = require("./utils/empty-writable");
9
9
  const run_program_1 = require("./run-program");
10
10
  const abort_on_signals_1 = require("./utils/abort-on-signals");
11
+ const terminal_prompt_1 = require("./utils/terminal-prompt");
11
12
  const abortCtrl = new AbortController();
12
13
  (0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
13
14
  let settled = false;
@@ -32,6 +33,7 @@ const secrets = (0, redact_1.createSecretRegistry)();
32
33
  stdout: process.stdout,
33
34
  stderr: process.stderr,
34
35
  stdoutColumns: process.stdout.columns,
36
+ prompt: (0, terminal_prompt_1.getTerminalPrompt)(process.env, abortCtrl.signal),
35
37
  }, process.argv, false).then(() => {
36
38
  settled = true;
37
39
  }, () => {
@@ -17,6 +17,7 @@ const units_1 = require("../utils/units");
17
17
  const service_tools_1 = require("../harness/service-tools");
18
18
  const path_1 = require("path");
19
19
  const check_keys_1 = require("../optimizer/check-keys");
20
+ const terminal_prompt_1 = require("../utils/terminal-prompt");
20
21
  function describeOutcome(planned) {
21
22
  switch (planned.outcome) {
22
23
  case 'published':
@@ -174,6 +175,125 @@ function registerWorkCommands(remote, context) {
174
175
  const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
175
176
  return { ...chosen, token };
176
177
  };
178
+ // publishes the bundle with where it comes from and says what it is now: published,
179
+ // already published, or only proposed
180
+ const publishBundle = async (serverName, server, token, bundle, annotations) => {
181
+ const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
182
+ ...bundle,
183
+ annotations,
184
+ });
185
+ const version = answer.body;
186
+ const files = Object.keys(bundle.files).length;
187
+ // a proposal that already waits answers 200 like a published version, with its state
188
+ const proposed = answer.status === 202 || version.state !== undefined;
189
+ const verb = answer.status === 202
190
+ ? 'proposed'
191
+ : proposed
192
+ ? 'already proposed'
193
+ : answer.status === 201
194
+ ? 'published'
195
+ : 'already published';
196
+ environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
197
+ const names = [...(version.tasks ?? []), ...(version.services ?? [])];
198
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
199
+ for (const line of annotationLines(version.annotations, 'from')) {
200
+ environment.stdout.write(`${line.trimStart()}\n`);
201
+ }
202
+ if (proposed) {
203
+ environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
204
+ }
205
+ return { version, proposed };
206
+ };
207
+ // what publishing the bundle would do, undefined from a server that can't say (before 1.27)
208
+ const planBundle = async (server, token, bundle) => {
209
+ try {
210
+ return (await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions/plan', bundle)).body;
211
+ }
212
+ catch (e) {
213
+ if (e instanceof server_client_1.ServerError && e.status === 404) {
214
+ return undefined;
215
+ }
216
+ throw e;
217
+ }
218
+ };
219
+ // The build file a run or up names, and what `--publish` may publish: the hash of the
220
+ // build file here, unless `--build` or HAMMERKIT_BUILD names a version.
221
+ const runDefinition = async (options) => {
222
+ const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
223
+ if (named !== undefined) {
224
+ if (options.publish) {
225
+ throw new Error(`--publish publishes the build file here, it does not go with ${options.build !== undefined ? '--build' : 'HAMMERKIT_BUILD'}`);
226
+ }
227
+ if (!bundle_1.HASH_PATTERN.test(named)) {
228
+ throw new Error(`${(0, server_client_1.printable)(named)} is not the hash of a published version (sha256:...)`);
229
+ }
230
+ return { definition: named };
231
+ }
232
+ const local = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
233
+ return { definition: (0, bundle_1.hashBundle)(local), local };
234
+ };
235
+ // Submits a run of a published version. When the server does not have the build file
236
+ // here, it is published first: with --publish at once, else when the person at the
237
+ // terminal says so. Without a terminal and without --publish the run fails and says how
238
+ // to publish it, so CI never waits on a question.
239
+ const submitPublishing = async (target, request, local, publish) => {
240
+ const { serverName, server, connection } = target;
241
+ try {
242
+ return await (0, sandbox_client_1.submitRun)(connection, request);
243
+ }
244
+ catch (e) {
245
+ const status = e.status;
246
+ if (!local || status !== 400 || !/no published build file/.test(String(e))) {
247
+ throw e;
248
+ }
249
+ }
250
+ const definition = request.definition;
251
+ const notPublished = `the build file here is not published on ${serverName} (${definition})`;
252
+ const token = await connection.token();
253
+ if (!publish) {
254
+ if (!environment.prompt) {
255
+ throw new Error(`${notPublished}, publish it first with: hammerkit remote publish ${serverName}, or pass --publish`);
256
+ }
257
+ // a server before 1.27 can't say what publishing would do: it is asked all the same,
258
+ // and refuses then what the caller may not do
259
+ const planned = await planBundle(server, token, local);
260
+ if (planned && planned.outcome === null) {
261
+ throw new Error(`${notPublished}, and you may only run published build files (builds: run): ` +
262
+ `somebody who may publish publishes it with: hammerkit remote publish ${serverName}`);
263
+ }
264
+ if (planned?.outcome !== 'already published') {
265
+ const files = Object.keys(local.files).length;
266
+ environment.stdout.write(`${notPublished}, ${files} ${files === 1 ? 'file' : 'files'}\n`);
267
+ if (planned) {
268
+ const names = [...(planned.tasks ?? []), ...(planned.services ?? [])];
269
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
270
+ }
271
+ const proposing = planned?.outcome === 'proposed';
272
+ if (proposing) {
273
+ environment.stdout.write(`${describeOutcome(planned)}\n`);
274
+ }
275
+ const answer = await environment.prompt(proposing ? `propose it to ${serverName} now? [y/N] ` : `publish it to ${serverName} now and go on? [y/N] `);
276
+ if (environment.abortCtrl.signal.aborted) {
277
+ throw new Error(`stopped, nothing was published`);
278
+ }
279
+ if (!(0, terminal_prompt_1.isYes)(answer)) {
280
+ throw new Error(`${notPublished}, nothing was published`);
281
+ }
282
+ }
283
+ }
284
+ const annotations = await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(local.files));
285
+ const { version, proposed } = await publishBundle(serverName, server, token, local, annotations).catch((e) => {
286
+ throw e instanceof server_client_1.ServerError && e.status === 403 ? new Error(`${notPublished}, ${e.message}`) : e;
287
+ });
288
+ if (version.hash !== definition) {
289
+ throw new Error(`${serverName} published ${(0, server_client_1.printable)(version.hash)}, not ${definition} as computed here, nothing was started`);
290
+ }
291
+ if (proposed) {
292
+ throw new Error(`${definition} is only proposed on ${serverName}, nothing was started: ` +
293
+ 'run this again once somebody who may publish approves it');
294
+ }
295
+ return (0, sandbox_client_1.submitRun)(connection, request);
296
+ };
177
297
  remote
178
298
  .command('publish [server]')
179
299
  .description('publish the build file and the files it includes to a server, runs refer to it by its hash')
@@ -198,23 +318,7 @@ function registerWorkCommands(remote, context) {
198
318
  }
199
319
  const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
200
320
  const { name: serverName, server, token } = await connect(name);
201
- const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
202
- ...bundle,
203
- annotations,
204
- });
205
- const version = answer.body;
206
- const files = Object.keys(bundle.files).length;
207
- const proposed = answer.status === 202;
208
- const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
209
- environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
210
- const names = [...(version.tasks ?? []), ...(version.services ?? [])];
211
- environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
212
- for (const line of annotationLines(version.annotations, 'from')) {
213
- environment.stdout.write(`${line.trimStart()}\n`);
214
- }
215
- if (proposed) {
216
- environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
217
- }
321
+ await publishBundle(serverName, server, token, bundle, annotations);
218
322
  }));
219
323
  remote
220
324
  .command('run <tasks...>')
@@ -229,12 +333,9 @@ function registerWorkCommands(remote, context) {
229
333
  .addOption(new commander_1.Option('--timeout <duration>', 'as --timeout of run: for tasks without their own, such as 30m'))
230
334
  .addOption(new commander_1.Option('--max-timeout <duration>', 'as --max-timeout of run: for every task, also one with its own timeout'))
231
335
  .addOption(new commander_1.Option('--detach', 'submit the run and exit, without following it: for a check that CI verifies later'))
336
+ .addOption(new commander_1.Option('--publish', 'publish the build file here first when the server does not have it, without asking; for CI and scripts'))
232
337
  .action(guarded('Remote run was not successful', async (tasks, options) => {
233
- const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
234
- const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
235
- if (!bundle_1.HASH_PATTERN.test(definition)) {
236
- throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
237
- }
338
+ const { definition, local } = await runDefinition(options);
238
339
  const source = await refSource(options);
239
340
  const ref = source.ref;
240
341
  const { name: serverName, server } = await getServer(options.server);
@@ -248,21 +349,7 @@ function registerWorkCommands(remote, context) {
248
349
  if (passthrough.length > 0) {
249
350
  runOptions.args = passthrough;
250
351
  }
251
- let submitted;
252
- try {
253
- submitted = await (0, sandbox_client_1.submitRun)(connection, {
254
- definition,
255
- tasks,
256
- source,
257
- options: runOptions,
258
- });
259
- }
260
- catch (e) {
261
- if (e.status === 400 && !named && /no published build file/.test(String(e))) {
262
- throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
263
- }
264
- throw e;
265
- }
352
+ const submitted = await submitPublishing({ serverName, server, connection }, { definition, tasks, source, options: runOptions }, local, !!options.publish);
266
353
  const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
267
354
  environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${tasks.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(ref)}${at}\n`);
268
355
  if (options.detach) {
@@ -314,17 +401,14 @@ function registerWorkCommands(remote, context) {
314
401
  .addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to start; the branch checked out here when left out'))
315
402
  .addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at; the commit checked out here when --ref is left out'))
316
403
  .addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
404
+ .addOption(new commander_1.Option('--publish', 'publish the build file here first when the server does not have it, without asking; for CI and scripts'))
317
405
  .action(guarded('Remote up was not successful', async (services, options) => {
318
- const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
319
- const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
320
- if (!bundle_1.HASH_PATTERN.test(definition)) {
321
- throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
322
- }
406
+ const { definition, local } = await runDefinition(options);
323
407
  const endpoints = (options.endpoint ?? []).map(service_tools_1.parseEndpoint);
324
408
  const source = await refSource(options);
325
409
  const { name: serverName, server } = await getServer(options.server);
326
410
  const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
327
- const submitted = await (0, sandbox_client_1.submitRun)(connection, {
411
+ const submitted = await submitPublishing({ serverName, server, connection }, {
328
412
  definition,
329
413
  services,
330
414
  endpoints,
@@ -333,7 +417,7 @@ function registerWorkCommands(remote, context) {
333
417
  ...(options.name ? { name: options.name } : {}),
334
418
  source,
335
419
  options: options.env ? { env: options.env } : {},
336
- });
420
+ }, local, !!options.publish);
337
421
  environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${services.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(source.ref)}${submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : ''}\n`);
338
422
  const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
339
423
  signal: environment.abortCtrl.signal,
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isYes = exports.getTerminalPrompt = exports.terminalPrompt = void 0;
4
+ const readline_1 = require("readline");
5
+ /**
6
+ * Asks the person at the terminal one question and answers the line they typed, or
7
+ * undefined when the input ended or the signal stopped the command first (Ctrl-C).
8
+ * The terminal stays in its own line mode, so Ctrl-C is a signal as anywhere else.
9
+ */
10
+ function terminalPrompt(input, output, signal) {
11
+ return (question) => new Promise((resolve) => {
12
+ if (signal.aborted) {
13
+ resolve(undefined);
14
+ return;
15
+ }
16
+ const lines = (0, readline_1.createInterface)({ input, output, terminal: false });
17
+ let answer;
18
+ const stop = () => lines.close();
19
+ signal.addEventListener('abort', stop, { once: true });
20
+ lines.once('close', () => {
21
+ signal.removeEventListener('abort', stop);
22
+ if (answer === undefined) {
23
+ // what was asked ends on a line of its own
24
+ output.write('\n');
25
+ }
26
+ resolve(answer);
27
+ });
28
+ lines.question(question, (line) => {
29
+ answer = line;
30
+ lines.close();
31
+ });
32
+ });
33
+ }
34
+ exports.terminalPrompt = terminalPrompt;
35
+ /**
36
+ * The prompt of the CLI: only when somebody sits at a terminal to answer it, never in
37
+ * CI, where a question would wait until the job times out.
38
+ */
39
+ function getTerminalPrompt(processEnvs, signal, stdio = process) {
40
+ if (!stdio.stdin.isTTY || !stdio.stdout.isTTY || isSet(processEnvs.CI)) {
41
+ return undefined;
42
+ }
43
+ return terminalPrompt(stdio.stdin, stdio.stdout, signal);
44
+ }
45
+ exports.getTerminalPrompt = getTerminalPrompt;
46
+ function isSet(value) {
47
+ return value !== undefined && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
48
+ }
49
+ function isYes(answer) {
50
+ return /^\s*y(es)?\s*$/i.test(answer ?? '');
51
+ }
52
+ exports.isYes = isYes;
53
+ //# sourceMappingURL=terminal-prompt.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "1.27.0-next.27",
4
+ "version": "1.27.0-next.28",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",