hammerkit 1.27.0-next.9 → 2.0.0-next.2

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.
Files changed (122) hide show
  1. package/build.schema.json +176 -6
  2. package/dist/cache/backends/local-cache-backend.js +6 -0
  3. package/dist/cache/backends/registry/registry-client.js +1 -0
  4. package/dist/cache/cache-sync.js +1 -1
  5. package/dist/cache/explain.js +17 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
  8. package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
  9. package/dist/dashboard/favicon.svg +1 -0
  10. package/dist/dashboard/index.html +15 -0
  11. package/dist/docker/item-network.js +13 -5
  12. package/dist/executer/archive-task-entry.js +4 -2
  13. package/dist/executer/deadline.js +17 -3
  14. package/dist/executer/deferred-restore.js +36 -0
  15. package/dist/executer/docker-service.js +30 -8
  16. package/dist/executer/docker-task.js +21 -1
  17. package/dist/executer/execute-work-service.js +2 -0
  18. package/dist/executer/execute-work-task.js +11 -4
  19. package/dist/executer/execution-steps.js +25 -4
  20. package/dist/executer/reset-work-tree.js +4 -0
  21. package/dist/executer/scheduler/enqueue-next.js +57 -6
  22. package/dist/executer/scheduler/state-key.js +42 -5
  23. package/dist/executer/watch-loop.js +6 -3
  24. package/dist/harness/connect.js +20 -6
  25. package/dist/harness/mcp-command.js +22 -6
  26. package/dist/harness/mcp.js +63 -114
  27. package/dist/harness/opencode.js +158 -50
  28. package/dist/harness/pi.js +29 -0
  29. package/dist/harness/sandbox-client.js +194 -23
  30. package/dist/harness/service-tools.js +141 -0
  31. package/dist/harness/task-tools.js +221 -0
  32. package/dist/kubernetes/apply.js +49 -1
  33. package/dist/kubernetes/await-running-state.js +172 -13
  34. package/dist/kubernetes/cluster-state.js +245 -0
  35. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
  36. package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
  37. package/dist/kubernetes/ensure-persistent-data.js +211 -6
  38. package/dist/kubernetes/follow-pod-logs.js +182 -0
  39. package/dist/kubernetes/job-placement.js +36 -12
  40. package/dist/kubernetes/kubernetes-instance.js +1 -0
  41. package/dist/kubernetes/network-policy.js +2 -1
  42. package/dist/kubernetes/remove-stale-items.js +46 -0
  43. package/dist/kubernetes/restore-kubernetes-data.js +9 -5
  44. package/dist/kubernetes/sandbox-runner.js +628 -0
  45. package/dist/kubernetes/service-rollouts.js +74 -0
  46. package/dist/kubernetes/store-kubernetes-data.js +5 -2
  47. package/dist/kubernetes/volumes.js +94 -9
  48. package/dist/main.js +2 -0
  49. package/dist/optimizer/calculate-checksum.js +3 -2
  50. package/dist/optimizer/check-key.js +94 -0
  51. package/dist/optimizer/check-keys.js +64 -0
  52. package/dist/optimizer/get-work-cache-stats.js +17 -3
  53. package/dist/optimizer/work-task-cache-description.js +49 -1
  54. package/dist/planner/service-definition.js +9 -4
  55. package/dist/planner/utils/append-work-dependencies.js +5 -3
  56. package/dist/planner/utils/append-work-service.js +18 -1
  57. package/dist/planner/utils/append-work-task.js +13 -3
  58. package/dist/planner/utils/get-work-runtime.js +6 -1
  59. package/dist/planner/utils/normalize-path.js +19 -1
  60. package/dist/planner/utils/parse-work-mount.js +7 -1
  61. package/dist/planner/utils/parse-work-port.js +4 -1
  62. package/dist/planner/validate.js +22 -0
  63. package/dist/planner/work-network.js +3 -1
  64. package/dist/planner/work-runtime-aggregate.js +32 -0
  65. package/dist/planner/work-runtime-kubernetes.js +136 -13
  66. package/dist/planner/work-task.js +6 -1
  67. package/dist/program.js +27 -9
  68. package/dist/remote/device-login.js +7 -2
  69. package/dist/remote/host-config.js +69 -3
  70. package/dist/remote/remote-commands.js +33 -7
  71. package/dist/remote/remote-status.js +112 -0
  72. package/dist/remote/remote-work-commands.js +562 -45
  73. package/dist/remote/server-client.js +15 -2
  74. package/dist/remote/token-source.js +64 -26
  75. package/dist/run-program.js +9 -0
  76. package/dist/schema/build-file-base-task-schema.js +3 -3
  77. package/dist/schema/build-file-container-service-schema.js +10 -0
  78. package/dist/schema/build-file-container-task-schema.js +16 -1
  79. package/dist/schema/build-file-include-schema.js +18 -4
  80. package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
  81. package/dist/schema/build-file-task-schema.js +5 -1
  82. package/dist/schema/capabilities-schema.js +56 -0
  83. package/dist/schema/reference-parser.js +119 -11
  84. package/dist/schema/removed-properties.js +20 -0
  85. package/dist/schema/schema-parser.js +94 -41
  86. package/dist/schema/work-scope-parser.js +33 -10
  87. package/dist/secrets/command-secret-provider.js +3 -1
  88. package/dist/secrets/github-app-token-command.js +3 -1
  89. package/dist/secrets/secret-catalog.js +43 -19
  90. package/dist/server/accounts-api.js +4 -1
  91. package/dist/server/approvals-api.js +1 -1
  92. package/dist/server/capacity-format.js +36 -0
  93. package/dist/server/capacity.js +300 -0
  94. package/dist/server/checks-api.js +83 -0
  95. package/dist/server/checks.js +116 -0
  96. package/dist/server/dashboard.js +196 -0
  97. package/dist/server/definitions-api.js +66 -3
  98. package/dist/server/endpoints.js +794 -0
  99. package/dist/server/executor.js +164 -9
  100. package/dist/server/kubernetes-backend.js +415 -48
  101. package/dist/server/kubernetes-sandbox-storage.js +46 -2
  102. package/dist/server/overview.js +271 -0
  103. package/dist/server/plan.js +170 -121
  104. package/dist/server/policy.js +34 -2
  105. package/dist/server/proposals-api.js +17 -8
  106. package/dist/server/repo-access.js +5 -0
  107. package/dist/server/run-logs.js +4 -0
  108. package/dist/server/run-plan.js +38 -4
  109. package/dist/server/run-request.js +58 -3
  110. package/dist/server/run-worker.js +262 -13
  111. package/dist/server/runs.js +962 -36
  112. package/dist/server/sandbox-pod.js +28 -0
  113. package/dist/server/sandbox-storage.js +23 -0
  114. package/dist/server/sandboxes.js +8 -0
  115. package/dist/server/server-config.js +246 -10
  116. package/dist/server/server.js +429 -32
  117. package/dist/server/service-runs.js +82 -0
  118. package/dist/server/timings.js +132 -0
  119. package/dist/server/wait-reason.js +21 -0
  120. package/dist/utils/tar.js +28 -1
  121. package/dist/utils/terminal-prompt.js +53 -0
  122. package/package.json +8 -2
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mayOpenEndpoints = exports.getRunRepo = exports.toRunEndpoints = exports.refuseEndpoints = exports.getEndpointHosts = exports.getServiceRunName = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const plan_1 = require("./plan");
6
+ const policy_1 = require("./policy");
7
+ const run_request_1 = require("./run-request");
8
+ // The name of a service run when the request names none: the branch it runs from, as a DNS
9
+ // label, so the runs of several branches stand side by side and each keeps its hosts.
10
+ function getServiceRunName(branch) {
11
+ const name = (branch ?? '')
12
+ .toLowerCase()
13
+ .replace(/[^a-z0-9-]+/g, '-')
14
+ .replace(/-+/g, '-')
15
+ .slice(0, 40)
16
+ .replace(/^-+|-+$/g, '');
17
+ return run_request_1.SERVICE_RUN_NAME_PATTERN.test(name) ? name : 'run';
18
+ }
19
+ exports.getServiceRunName = getServiceRunName;
20
+ /**
21
+ * The host labels of a run's endpoints: its name, the port and twelve hex digits of its
22
+ * owner (at most 40 + 5 + 2 + 12 and three dashes: a DNS label), so two callers with a branch of the same name get hosts of their own, and a run
23
+ * that replaces another of the same name takes over its hosts. Two endpoints on the same
24
+ * port (of two services) are told apart by their place.
25
+ */
26
+ function getEndpointHosts(name, owner, endpoints) {
27
+ const of = (0, crypto_1.createHash)('sha256').update(`${owner.iss}\0${owner.sub}`).digest('hex').slice(0, 12);
28
+ return endpoints.map((endpoint, index) => endpoints.findIndex((other) => other.port === endpoint.port) === index
29
+ ? `${name}-${endpoint.port}-${of}`
30
+ : `${name}-${endpoint.port}-${index}-${of}`);
31
+ }
32
+ exports.getEndpointHosts = getEndpointHosts;
33
+ // The container port of each `host:container` port of a planned service.
34
+ function containerPorts(plan) {
35
+ return plan.ports.map((port) => Number(port.split(':').pop()));
36
+ }
37
+ /**
38
+ * Why the endpoints of a service run can't be published, or null. Each is a port its
39
+ * service declares, of a service the run starts. Under a `runs.secretNetwork` below `open`
40
+ * a service behind an endpoint may not read a secret or a credential of the server: a page
41
+ * is a way out that no mask of the log covers.
42
+ */
43
+ function refuseEndpoints(request, plan, secretNetworkOpen) {
44
+ for (const endpoint of request.endpoints) {
45
+ const item = plan[endpoint.service];
46
+ if (!item || item.kind !== 'service') {
47
+ return `the endpoint ${endpoint.service}:${endpoint.port} names no service the run starts`;
48
+ }
49
+ if (!containerPorts(item).includes(endpoint.port)) {
50
+ const ports = containerPorts(item);
51
+ return `${endpoint.service} has no port ${endpoint.port}${ports.length > 0 ? `, its ports are ${ports.join(', ')}` : ', it declares none'}`;
52
+ }
53
+ const reads = (0, plan_1.getCredentialReads)(item);
54
+ if (!secretNetworkOpen && reads.length > 0) {
55
+ return `${endpoint.service} reads ${reads.join(', ')}, it can not be behind an endpoint on this server (runs.secretNetwork): a page could show it`;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ exports.refuseEndpoints = refuseEndpoints;
61
+ function toRunEndpoints(endpoints, hosts, url) {
62
+ return endpoints.map((endpoint, index) => ({
63
+ service: endpoint.service,
64
+ port: endpoint.port,
65
+ host: hosts[index],
66
+ url: url(hosts[index]),
67
+ }));
68
+ }
69
+ exports.toRunEndpoints = toRunEndpoints;
70
+ // The repository of a run's source, which the endpoints grants are written for.
71
+ function getRunRepo(run) {
72
+ return run.source.type === 'upload' ? null : run.source.repo;
73
+ }
74
+ exports.getRunRepo = getRunRepo;
75
+ // Who may open the endpoints of a service run, and extend it, besides its owner: those
76
+ // with an `open` or `publish` level for its repository.
77
+ function mayOpenEndpoints(grant, run) {
78
+ const repo = getRunRepo(run);
79
+ return repo !== null && (0, policy_1.endpointLevel)(grant, repo) !== null;
80
+ }
81
+ exports.mayOpenEndpoints = mayOpenEndpoints;
82
+ //# sourceMappingURL=service-runs.js.map
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTaskStats = exports.roughly = exports.ranMs = exports.waitedMs = void 0;
4
+ const at = (iso) => (iso ? Date.parse(iso) : NaN);
5
+ // How long a run waited in the queue for a place: from when it was queued (after its
6
+ // approval, or when it woke) until it started, or until now while it waits.
7
+ function waitedMs(run, now) {
8
+ const queued = at(run.queuedAt);
9
+ if (!Number.isFinite(queued)) {
10
+ return undefined;
11
+ }
12
+ const started = at(run.startedAt);
13
+ if (Number.isFinite(started) && started >= queued) {
14
+ return started - queued;
15
+ }
16
+ if (run.state === 'queued') {
17
+ return Math.max(0, now - queued);
18
+ }
19
+ // it ended before it started: cancelled in the queue
20
+ const finished = at(run.finishedAt);
21
+ return Number.isFinite(finished) && finished >= queued ? finished - queued : undefined;
22
+ }
23
+ exports.waitedMs = waitedMs;
24
+ // How long a run ran: from its start to its end, or until now while it runs. A service run
25
+ // counts until its services were up.
26
+ function ranMs(run, now) {
27
+ const started = at(run.startedAt);
28
+ if (!Number.isFinite(started)) {
29
+ return undefined;
30
+ }
31
+ const up = at(run.upAt);
32
+ const finished = at(run.finishedAt);
33
+ const end = Number.isFinite(up) && up >= started ? up : Number.isFinite(finished) ? finished : now;
34
+ return Math.max(0, end - started);
35
+ }
36
+ exports.ranMs = ranMs;
37
+ // a span of time as a person reads it at a glance: 2d 3h, 7h 59m, 29m 5s, 12s, 340ms
38
+ function roughly(ms) {
39
+ if (ms < 1000) {
40
+ return `${Math.max(0, Math.round(ms))}ms`;
41
+ }
42
+ const seconds = Math.round(ms / 1000);
43
+ const [d, h, m, s] = [
44
+ Math.floor(seconds / 86400),
45
+ Math.floor(seconds / 3600) % 24,
46
+ Math.floor(seconds / 60) % 60,
47
+ seconds % 60,
48
+ ];
49
+ if (d > 0) {
50
+ return `${d}d ${h}h`;
51
+ }
52
+ if (h > 0) {
53
+ return `${h}h ${m}m`;
54
+ }
55
+ if (m > 0) {
56
+ return s > 0 && m < 10 ? `${m}m ${s}s` : `${m}m`;
57
+ }
58
+ return `${s}s`;
59
+ }
60
+ exports.roughly = roughly;
61
+ function quantile(sorted, q) {
62
+ if (sorted.length === 0) {
63
+ return undefined;
64
+ }
65
+ return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(q * sorted.length) - 1))];
66
+ }
67
+ /**
68
+ * Per task and repository, newest first: how often it ran and how, what it took the last
69
+ * time, and the median and 90th percentile of the times it executed. `runs` is newest first.
70
+ */
71
+ function getTaskStats(runs, now) {
72
+ const groups = new Map();
73
+ for (const run of runs) {
74
+ const repo = run.source.repo ?? 'an upload';
75
+ const waited = waitedMs(run, now);
76
+ for (const [task, timing] of Object.entries(run.timings ?? {})) {
77
+ if (timing.status === 'running' || timing.status === 'skipped' || timing.status === 'cancelled') {
78
+ continue;
79
+ }
80
+ const key = `${repo}\0${task}`;
81
+ let group = groups.get(key);
82
+ if (!group) {
83
+ group = {
84
+ stats: {
85
+ repo,
86
+ task,
87
+ runs: 0,
88
+ succeeded: 0,
89
+ failed: 0,
90
+ cached: 0,
91
+ last: {
92
+ status: timing.status,
93
+ at: timing.finishedAt ?? run.finishedAt ?? run.createdAt,
94
+ ...(timing.durationMs !== undefined ? { durationMs: timing.durationMs } : {}),
95
+ },
96
+ },
97
+ durations: [],
98
+ waits: [],
99
+ };
100
+ groups.set(key, group);
101
+ }
102
+ const { stats } = group;
103
+ stats.runs++;
104
+ if (timing.status === 'cached') {
105
+ stats.cached++;
106
+ continue;
107
+ }
108
+ timing.status === 'succeeded' ? stats.succeeded++ : stats.failed++;
109
+ if (timing.durationMs !== undefined) {
110
+ group.durations.push(timing.durationMs);
111
+ }
112
+ if (waited !== undefined) {
113
+ group.waits.push(waited);
114
+ }
115
+ }
116
+ }
117
+ return [...groups.values()].map(({ stats, durations, waits }) => {
118
+ durations.sort((a, b) => a - b);
119
+ waits.sort((a, b) => a - b);
120
+ const median = quantile(durations, 0.5);
121
+ const p90 = quantile(durations, 0.9);
122
+ const wait = quantile(waits, 0.5);
123
+ return {
124
+ ...stats,
125
+ ...(median !== undefined ? { medianMs: median } : {}),
126
+ ...(p90 !== undefined ? { p90Ms: p90 } : {}),
127
+ ...(wait !== undefined ? { medianWaitMs: wait } : {}),
128
+ };
129
+ });
130
+ }
131
+ exports.getTaskStats = getTaskStats;
132
+ //# sourceMappingURL=timings.js.map
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeWait = void 0;
4
+ // What keeps a queued run from starting, in words: `mine` says whose limit it is, `keys`
5
+ // names the setting of the server that sets it.
6
+ function describeWait(reason, mine, keys = true) {
7
+ switch (reason.type) {
8
+ case 'own-limit':
9
+ return `${mine ? 'your' : 'its owner’s'} limit of running runs, ${reason.running} of ${reason.limit}${keys ? ' (runs.limits.running)' : ''}`;
10
+ case 'sandbox':
11
+ return reason.run ? `its sandbox runs ${reason.run}` : 'another run works on its sandbox';
12
+ case 'held':
13
+ return reason.run ? `${reason.run} holds what it needs` : 'another run holds what it needs';
14
+ case 'place':
15
+ return `no free place, ${reason.used} of ${reason.max} taken${keys ? ' (runs.maxConcurrent)' : ''}`;
16
+ case 'starting':
17
+ return 'starts now';
18
+ }
19
+ }
20
+ exports.describeWait = describeWait;
21
+ //# sourceMappingURL=wait-reason.js.map
package/dist/utils/tar.js CHANGED
@@ -3,7 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.packArchive = void 0;
4
4
  const tar_1 = require("tar");
5
5
  function packArchive(options, files) {
6
- return (0, tar_1.create)({ ...options, jobs: 1 }, files);
6
+ return (0, tar_1.create)({ ...options, jobs: 1, linkCache: new ExactLinkCache() }, files);
7
7
  }
8
8
  exports.packArchive = packArchive;
9
+ // node-tar packs a file with more than one link as a link to the first file it packed with
10
+ // the same `dev:ino`. On Windows a file id has 64 bits, and Node gives it as a number, so
11
+ // ids above 2^53 are rounded and files of two link groups can get the same one. Packed as
12
+ // a link, a file would be restored with the other file's content. This cache leaves out the
13
+ // ids that may be rounded, so those files are packed in full.
14
+ //
15
+ // node-tar asks the cache in two places. A WriteEntry links to the path it gets only when
16
+ // that path is in the packed directory, and packs the file in full otherwise. The Pack treats
17
+ // a file it gets no path for as the first of its links, and takes a path for it that ends
18
+ // the archive a second time when the file is empty and the last one packed: an empty file
19
+ // whose other link is outside the packed directory, as in a pnpm node_modules, or any empty
20
+ // file with a rounded id. That throws "write after end" (a zlib stream error with gzip)
21
+ // where nothing can catch it. With one file read at a time that path is not needed, so the
22
+ // cache never answers with nothing: for a file it has no path for, it answers NOT_PACKED,
23
+ // which is in no directory.
24
+ class ExactLinkCache extends Map {
25
+ get(key) {
26
+ return super.get(key) || NOT_PACKED;
27
+ }
28
+ set(key, path) {
29
+ return isExact(key) ? super.set(key, path) : this;
30
+ }
31
+ }
32
+ const NOT_PACKED = '\0';
33
+ function isExact(key) {
34
+ return key.split(':').every((part) => Number.isSafeInteger(Number(part)));
35
+ }
9
36
  //# sourceMappingURL=tar.js.map
@@ -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.9",
4
+ "version": "2.0.0-next.2",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",
@@ -22,6 +22,11 @@
22
22
  "build.schema.json",
23
23
  "harness"
24
24
  ],
25
+ "pkg": {
26
+ "assets": [
27
+ "dist/dashboard/**/*"
28
+ ]
29
+ },
25
30
  "pi": {
26
31
  "extensions": [
27
32
  "./harness/pi.ts"
@@ -43,8 +48,9 @@
43
48
  "type": "git"
44
49
  },
45
50
  "scripts": {
46
- "prepack": "tsc -b tsconfig.json",
51
+ "prepack": "tsc -b tsconfig.json && npm run build:dashboard",
47
52
  "build": "tsc -b tsconfig.json",
53
+ "build:dashboard": "npm ci --prefix dashboard --no-audit --no-fund && npm run build --prefix dashboard",
48
54
  "test": "vitest run",
49
55
  "test:integration": "vitest run --mode integration",
50
56
  "test:mutation": "stryker run",