hammerkit 1.16.0 → 1.18.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.js CHANGED
@@ -16,6 +16,7 @@ const reset_work_tree_1 = require("./executer/reset-work-tree");
16
16
  const execute_work_tree_1 = require("./executer/execute-work-tree");
17
17
  const package_1 = require("./docker/package");
18
18
  const explain_1 = require("./cache/explain");
19
+ const describe_task_1 = require("./planner/describe-task");
19
20
  const work_graph_serializer_1 = require("./planner/work-graph-serializer");
20
21
  const dry_run_1 = require("./executer/dry-run");
21
22
  const cache_sync_1 = require("./cache/cache-sync");
@@ -127,6 +128,9 @@ class Cli {
127
128
  }
128
129
  // Read-only cache prediction for every task in scope: executes no command,
129
130
  // starts no container/service, performs no cache push/pull.
131
+ explainDefinition() {
132
+ return [...(0, plan_work_tasks_1.iterateWorkTasks)(this.workTree)].map((task) => (0, describe_task_1.describeTask)(task, this.environment.cwd));
133
+ }
130
134
  async explain(options) {
131
135
  return (0, explain_1.explainWorkTree)(this.workTree, options?.cacheDefault ?? 'checksum', this.environment);
132
136
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.cleanCache = exports.storeCache = exports.getArchivePaths = exports.restoreCache = void 0;
4
+ const resolve_git_source_1 = require("../includes/resolve-git-source");
4
5
  const remove_orphaned_containers_1 = require("../docker/remove-orphaned-containers");
5
6
  const execute_docker_1 = require("./execute-docker");
6
7
  const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
@@ -92,6 +93,10 @@ async function cleanCache(workTree, environment, options) {
92
93
  await service.data.caching.backend.clear(service.id(), environment);
93
94
  }
94
95
  }
96
+ if (options?.cache) {
97
+ // cached remote includes are kept like stored results, so they go with them
98
+ await (0, resolve_git_source_1.purgeGitSources)();
99
+ }
95
100
  if (workTree.environment.type === 'docker') {
96
101
  const removed = await (0, remove_orphaned_containers_1.removeOrphanedContainers)((0, execute_docker_1.getContainerCli)(workTree.environment));
97
102
  for (const id of removed) {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isWithin = exports.resolveGitSource = exports.describeGitSource = exports.getIncludesDirectory = void 0;
3
+ exports.isWithin = exports.purgeGitSources = exports.refreshGitSource = exports.resolveGitSource = exports.describeGitSource = exports.getIncludesDirectory = void 0;
4
4
  const child_process_1 = require("child_process");
5
5
  const crypto_1 = require("crypto");
6
6
  const promises_1 = require("fs/promises");
@@ -34,6 +34,34 @@ async function resolveGitSource(source, environment) {
34
34
  if (cached) {
35
35
  return cached;
36
36
  }
37
+ return fetchIntoCache(source, directory, environment, false);
38
+ }
39
+ exports.resolveGitSource = resolveGitSource;
40
+ // `hammerkit includes pull`: fetch a source again and replace the cached copy
41
+ // with the ref's current commit. The old copy stays when the fetch fails.
42
+ async function refreshGitSource(source, environment) {
43
+ const directory = (0, path_1.join)(getIncludesDirectory(), getSourceKey(source));
44
+ const cached = await readCached(directory);
45
+ const pinned = isCommitSha(source.ref);
46
+ const resolved = pinned && cached ? cached : await fetchIntoCache(source, directory, environment, cached !== null);
47
+ return {
48
+ git: source.git,
49
+ ref: source.ref ?? null,
50
+ previous: cached?.commit ?? null,
51
+ commit: resolved.commit,
52
+ pinned,
53
+ };
54
+ }
55
+ exports.refreshGitSource = refreshGitSource;
56
+ // `hammerkit clean --cache`: the next run fetches every source again.
57
+ async function purgeGitSources() {
58
+ await (0, promises_1.rm)(getIncludesDirectory(), { recursive: true, force: true });
59
+ }
60
+ exports.purgeGitSources = purgeGitSources;
61
+ function isCommitSha(ref) {
62
+ return ref !== undefined && /^[0-9a-f]{40}$/i.test(ref);
63
+ }
64
+ async function fetchIntoCache(source, directory, environment, replace) {
37
65
  await (0, promises_1.mkdir)(getIncludesDirectory(), { recursive: true });
38
66
  const staging = `${directory}.${(0, crypto_1.randomUUID)()}`;
39
67
  try {
@@ -41,6 +69,9 @@ async function resolveGitSource(source, environment) {
41
69
  const commit = await fetchInto((0, path_1.join)(staging, 'repo'), source, environment);
42
70
  const stored = { git: source.git, ref: source.ref ?? null, commit };
43
71
  await (0, promises_1.writeFile)((0, path_1.join)(staging, 'source.json'), JSON.stringify(stored, null, 2));
72
+ if (replace) {
73
+ await (0, promises_1.rm)(directory, { recursive: true, force: true });
74
+ }
44
75
  try {
45
76
  await (0, promises_1.rename)(staging, directory);
46
77
  }
@@ -61,7 +92,6 @@ async function resolveGitSource(source, environment) {
61
92
  }
62
93
  return resolved;
63
94
  }
64
- exports.resolveGitSource = resolveGitSource;
65
95
  async function readCached(directory) {
66
96
  try {
67
97
  const stored = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(directory, 'source.json'), 'utf8'));
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeTask = void 0;
4
+ const path_1 = require("path");
5
+ const work_task_1 = require("./work-task");
6
+ const toPosix = (value) => (path_1.sep === path_1.posix.sep ? value : value.split(path_1.sep).join(path_1.posix.sep));
7
+ function describeTask(item, cwd) {
8
+ const task = item.data;
9
+ const remote = task.scope.remote;
10
+ return {
11
+ name: item.name,
12
+ type: (0, work_task_1.isContainerWorkTask)(task) ? 'container' : 'local',
13
+ description: task.description || null,
14
+ source: {
15
+ file: toPosix(remote ? (0, path_1.relative)(remote.root, task.scope.fileName) : (0, path_1.relative)(cwd, task.scope.fileName)),
16
+ includedAs: task.scope.namePrefix,
17
+ git: remote?.git ?? null,
18
+ ref: remote?.ref ?? null,
19
+ commit: remote?.commit ?? null,
20
+ },
21
+ image: (0, work_task_1.isContainerWorkTask)(task) ? task.image : null,
22
+ shell: task.shell,
23
+ cmds: task.cmds.map((cmd) => cmd.cmd),
24
+ src: task.src.map((src) => src.source),
25
+ generates: task.generates.map((generate) => (0, path_1.relative)(task.cwd, generate.path)),
26
+ deps: item.deps.map((dep) => dep.name),
27
+ needs: item.needs.map((need) => need.name),
28
+ mounts: (0, work_task_1.isContainerWorkTask)(task) ? task.mounts.map((mount) => mount.mount) : [],
29
+ envs: Object.entries(task.envs.variables).map(([name, value]) => ({
30
+ name,
31
+ value,
32
+ origin: task.envOrigins[name] ?? 'task',
33
+ })),
34
+ envReferences: task.envs.replacements.map((replacement) => ({
35
+ name: replacement.key,
36
+ from: replacement.name,
37
+ available: replacement.available,
38
+ })),
39
+ };
40
+ }
41
+ exports.describeTask = describeTask;
42
+ //# sourceMappingURL=describe-task.js.map
@@ -56,6 +56,7 @@ function parseTask(cwd, task, environment, context) {
56
56
  const envs = (0, replace_env_variables_1.buildEnvironmentVariables)(task.envs, environment, context);
57
57
  const baseWorkNode = {
58
58
  envs,
59
+ envOrigins: getEnvOrigins(task),
59
60
  description: (0, template_value_1.templateValue)(task.schema.description || '', envs).trim(),
60
61
  name: task.relativeName,
61
62
  cwd,
@@ -92,4 +93,22 @@ function parseTask(cwd, task, environment, context) {
92
93
  };
93
94
  }
94
95
  }
96
+ function getEnvOrigins(task) {
97
+ const origins = {};
98
+ for (const key of Object.keys(task.envs)) {
99
+ if (key in (task.schema.envs ?? {})) {
100
+ origins[key] = 'task';
101
+ }
102
+ else if (key in (task.scope.inputs ?? {}) && key in (task.scope.schema.envs ?? {})) {
103
+ origins[key] = 'input';
104
+ }
105
+ else if (key in (task.scope.schema.envs ?? {})) {
106
+ origins[key] = 'build-file';
107
+ }
108
+ else {
109
+ origins[key] = 'extend';
110
+ }
111
+ }
112
+ return origins;
113
+ }
95
114
  //# sourceMappingURL=append-work-task.js.map
package/dist/program.js CHANGED
@@ -32,6 +32,51 @@ async function createCli(fileName, environment, workScope) {
32
32
  return (0, cli_1.getCli)(workTree, environment);
33
33
  }
34
34
  exports.createCli = createCli;
35
+ const ENV_ORIGINS = {
36
+ task: () => 'task',
37
+ input: (includedAs) => `input of include ${includedAs}`,
38
+ 'build-file': () => 'build file',
39
+ extend: () => 'extended task',
40
+ };
41
+ function printTaskDefinition(environment, definition) {
42
+ environment.stdout.write(`• ${definition.name}\n`);
43
+ const source = definition.source;
44
+ if (source.git) {
45
+ (0, log_1.printProperty)(environment, 'defined in', `${source.file} of ${source.git} at ${source.ref ?? 'HEAD'} (commit ${(source.commit ?? '').slice(0, 12)})`);
46
+ }
47
+ else {
48
+ (0, log_1.printProperty)(environment, 'defined in', source.file);
49
+ }
50
+ if (source.includedAs) {
51
+ (0, log_1.printProperty)(environment, 'included as', source.includedAs);
52
+ }
53
+ if (definition.description) {
54
+ (0, log_1.printProperty)(environment, 'description', definition.description);
55
+ }
56
+ if (definition.image) {
57
+ (0, log_1.printProperty)(environment, 'image', definition.image);
58
+ }
59
+ for (const [name, values] of [
60
+ ['deps', definition.deps],
61
+ ['needs', definition.needs],
62
+ ['src', definition.src],
63
+ ['generates', definition.generates],
64
+ ['mounts', definition.mounts],
65
+ ]) {
66
+ if (values.length > 0) {
67
+ (0, log_1.printProperty)(environment, name, values.join(', '));
68
+ }
69
+ }
70
+ for (const cmd of definition.cmds) {
71
+ (0, log_1.printProperty)(environment, 'cmd', cmd);
72
+ }
73
+ for (const env of definition.envs) {
74
+ (0, log_1.printProperty)(environment, `env ${env.name}`, `${env.value} ${colors_1.default.grey(`(${ENV_ORIGINS[env.origin](source.includedAs)})`)}`);
75
+ }
76
+ for (const reference of definition.envReferences) {
77
+ (0, log_1.printProperty)(environment, `env ${reference.name}`, `from $${reference.from} ${colors_1.default.grey(reference.available ? '(set)' : '(not set)')}`);
78
+ }
79
+ }
35
80
  function parseWorkLabelScope(options) {
36
81
  const scope = {
37
82
  environmentName: null,
@@ -278,9 +323,22 @@ async function getProgram(environment, argv) {
278
323
  .choices(['checksum', 'modify-date', 'none']))
279
324
  .addOption(new commander_1.Option('--json', 'emit the explanation as JSON').default(false))
280
325
  .addOption(new commander_1.Option('--check', 'exit with code 1 when any task would be a cache miss').default(false))
326
+ .addOption(new commander_1.Option('--definition', 'show the resolved definition of the tasks and where it came from instead').default(false))
281
327
  .action(async (task, options) => {
282
328
  try {
283
329
  const cli = await createCli(fileName, environment, task ? { taskName: task, environmentName: options.env ?? null } : parseWorkLabelScope(options));
330
+ if (options.definition) {
331
+ const definitions = cli.explainDefinition();
332
+ if (options.json) {
333
+ environment.stdout.write(`${JSON.stringify(definitions, null, 2)}\n`);
334
+ }
335
+ else {
336
+ for (const definition of definitions) {
337
+ printTaskDefinition(environment, definition);
338
+ }
339
+ }
340
+ return;
341
+ }
284
342
  const explanations = await cli.explain({ cacheDefault: options.cache });
285
343
  const missed = explanations.filter((explanation) => explanation.status === 'miss');
286
344
  if (options.json) {
@@ -313,6 +371,43 @@ async function getProgram(environment, argv) {
313
371
  program.error('Explain was not successful', { exitCode: 1 });
314
372
  }
315
373
  });
374
+ program
375
+ .command('includes')
376
+ .description('manage remote includes (includes pull)')
377
+ .command('pull')
378
+ .description('fetch the git repositories of remote includes again, picking up new commits of branches and tags')
379
+ .action(async () => {
380
+ try {
381
+ const { ctx } = await (0, schema_parser_1.createParseContext)(fileName, environment, { refresh: true });
382
+ const refreshed = [...(ctx.refreshed?.values() ?? [])];
383
+ for (const source of refreshed) {
384
+ const name = `${source.git} at ${source.ref ?? 'HEAD'}`;
385
+ const commit = source.commit.slice(0, 12);
386
+ if (source.pinned) {
387
+ environment.stdout.write(`${name}: pinned to ${commit}\n`);
388
+ }
389
+ else if (source.previous === source.commit) {
390
+ environment.stdout.write(`${name}: up to date at ${commit}\n`);
391
+ }
392
+ else if (source.previous === null) {
393
+ environment.stdout.write(`${name}: fetched ${commit}\n`);
394
+ }
395
+ else {
396
+ environment.stdout.write(`${name}: ${source.previous.slice(0, 12)} -> ${commit}\n`);
397
+ }
398
+ }
399
+ if (refreshed.length === 0) {
400
+ environment.stdout.write('no remote includes\n');
401
+ }
402
+ }
403
+ catch (e) {
404
+ if (e instanceof commander_1.CommanderError) {
405
+ throw e;
406
+ }
407
+ environment.console.error((0, log_1.getErrorMessage)(e));
408
+ program.error('Includes pull was not successful', { exitCode: 1 });
409
+ }
410
+ });
316
411
  const cacheCommand = program.command('cache').description('inspect, prune and move cache entries');
317
412
  for (const direction of ['pull', 'push']) {
318
413
  cacheCommand
@@ -8,9 +8,10 @@ const default_build_file_1 = require("../parser/default-build-file");
8
8
  const parse_error_1 = require("./parse-error");
9
9
  const build_file_include_schema_1 = require("./build-file-include-schema");
10
10
  const resolve_git_source_1 = require("../includes/resolve-git-source");
11
- async function createParseContext(fileName, environment) {
11
+ async function createParseContext(fileName, environment, options) {
12
12
  const ctx = {
13
13
  files: {},
14
+ refreshed: options?.refresh ? new Map() : undefined,
14
15
  };
15
16
  const scope = await appendBuildFile((0, path_1.dirname)(fileName), ctx, environment, fileName, []);
16
17
  return { ctx, scope };
@@ -32,12 +33,13 @@ async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remo
32
33
  schema: inputs ? applyInputs(result.data, inputs, namePrefix, fileName) : result.data,
33
34
  namePrefix: relativeName,
34
35
  remote,
36
+ inputs,
35
37
  references: {},
36
38
  };
37
39
  ctx.files[key] = scope;
38
40
  if (scope.schema.references) {
39
41
  for (const referenceName of Object.keys(scope.schema.references)) {
40
- const target = await locateInclude(scope, scope.schema.references[referenceName],
42
+ const target = await locateInclude(ctx, scope, scope.schema.references[referenceName],
41
43
  // a remote file's paths resolve within its repository
42
44
  remote ? (0, path_1.dirname)(scope.fileName) : scope.cwd, environment);
43
45
  const referenceScope = await appendBuildFile((0, path_1.dirname)(target.fileName), ctx, environment, target.fileName, [...namePrefix, referenceName], target.remote, target.inputs);
@@ -49,7 +51,7 @@ async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remo
49
51
  }
50
52
  if (scope.schema.includes) {
51
53
  for (const includeName of Object.keys(scope.schema.includes)) {
52
- const target = await locateInclude(scope, scope.schema.includes[includeName], (0, path_1.dirname)(scope.fileName), environment);
54
+ const target = await locateInclude(ctx, scope, scope.schema.includes[includeName], (0, path_1.dirname)(scope.fileName), environment);
53
55
  const includeScope = await appendBuildFile(cwd, ctx, environment, target.fileName, [...namePrefix, includeName], target.remote, target.inputs);
54
56
  if (scope.references[includeName]) {
55
57
  throw new Error(includeName + ' already exists');
@@ -67,9 +69,9 @@ exports.appendBuildFile = appendBuildFile;
67
69
  // A string is a local path below `base`; an object is a git source, fetched once
68
70
  // into the include cache. Inside a cached checkout a local path may not leave it:
69
71
  // a remote file contributes definitions from its own repository only.
70
- async function locateInclude(scope, include, base, environment) {
72
+ async function locateInclude(ctx, scope, include, base, environment) {
71
73
  if ((0, build_file_include_schema_1.isGitSource)(include)) {
72
- const remote = await (0, resolve_git_source_1.resolveGitSource)(include, environment);
74
+ const remote = await resolveRemote(ctx, include, environment);
73
75
  const path = (0, path_1.join)(remote.root, include.path ?? '');
74
76
  if (!(0, resolve_git_source_1.isWithin)(remote.root, path)) {
75
77
  throw new Error(`${include.path} in ${include.git} points outside the repository`);
@@ -82,6 +84,15 @@ async function locateInclude(scope, include, base, environment) {
82
84
  }
83
85
  return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote };
84
86
  }
87
+ // `includes pull` fetches each source once, however often the build files name it.
88
+ async function resolveRemote(ctx, source, environment) {
89
+ const key = `${source.git}\n${source.ref ?? ''}`;
90
+ if (!ctx.refreshed || ctx.refreshed.has(key)) {
91
+ return (0, resolve_git_source_1.resolveGitSource)(source, environment);
92
+ }
93
+ ctx.refreshed.set(key, await (0, resolve_git_source_1.refreshGitSource)(source, environment));
94
+ return (0, resolve_git_source_1.resolveGitSource)(source, environment);
95
+ }
85
96
  // The top-level `envs` of an included file are its inputs: it declares each one
86
97
  // with a default, and `with` replaces the default. A name the file does not
87
98
  // declare is rejected, so a typo cannot silently do nothing.
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.16.0",
4
+ "version": "1.18.0",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",