hammerkit 1.27.0-next.16 → 1.27.0-next.18

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 (37) hide show
  1. package/dist/executer/execution-steps.js +5 -2
  2. package/dist/executer/scheduler/state-key.js +37 -5
  3. package/dist/harness/mcp.js +2 -1
  4. package/dist/harness/opencode.js +9 -2
  5. package/dist/harness/pi.js +15 -0
  6. package/dist/harness/sandbox-client.js +26 -2
  7. package/dist/harness/task-tools.js +12 -1
  8. package/dist/optimizer/calculate-checksum.js +3 -2
  9. package/dist/optimizer/check-key.js +94 -0
  10. package/dist/optimizer/check-keys.js +64 -0
  11. package/dist/optimizer/get-work-cache-stats.js +17 -3
  12. package/dist/optimizer/work-task-cache-description.js +49 -1
  13. package/dist/planner/utils/normalize-path.js +19 -1
  14. package/dist/planner/utils/parse-work-mount.js +7 -1
  15. package/dist/planner/validate.js +22 -0
  16. package/dist/remote/remote-work-commands.js +70 -0
  17. package/dist/remote/server-client.js +15 -2
  18. package/dist/run-program.js +9 -0
  19. package/dist/schema/reference-parser.js +8 -1
  20. package/dist/secrets/github-app-token-command.js +3 -1
  21. package/dist/server/accounts-api.js +1 -0
  22. package/dist/server/checks-api.js +83 -0
  23. package/dist/server/checks.js +116 -0
  24. package/dist/server/executor.js +41 -3
  25. package/dist/server/kubernetes-backend.js +1 -1
  26. package/dist/server/kubernetes-sandbox-storage.js +41 -1
  27. package/dist/server/plan.js +103 -78
  28. package/dist/server/policy.js +10 -2
  29. package/dist/server/run-request.js +12 -1
  30. package/dist/server/run-worker.js +16 -2
  31. package/dist/server/runs.js +18 -0
  32. package/dist/server/sandbox-pod.js +28 -0
  33. package/dist/server/sandbox-storage.js +23 -0
  34. package/dist/server/sandboxes.js +8 -0
  35. package/dist/server/server-config.js +14 -1
  36. package/dist/server/server.js +46 -3
  37. package/package.json +1 -1
@@ -89,8 +89,11 @@ exports.prepareMounts = prepareMounts;
89
89
  async function setUserPermissions(item, container, environment) {
90
90
  if (item.data.user) {
91
91
  // On a server only the volumes: a chown as the container's root on a path of the host
92
- // would follow a link a task put there after the binds were checked
93
- const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !environment.hosted || !(0, path_1.isAbsolute)(bind.localPath));
92
+ // would follow a link a task put there after the binds were checked. Never a mount: it
93
+ // is a path of the host the build file names, such as /var/run/docker.sock, whose owner
94
+ // is not hammerkit's to change
95
+ const mounted = new Set(item.data.mounts.map((mount) => mount.containerPath));
96
+ const binds = (0, get_container_binds_1.getContainerBinds)(item).filter((bind) => !mounted.has(bind.containerPath) && (!environment.hosted || !(0, path_1.isAbsolute)(bind.localPath)));
94
97
  if (!environment.hosted) {
95
98
  await (0, set_user_permission_1.setUserPermission)(item.data.cwd, item.status, environment, container, item.data.user);
96
99
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.computeStateKey = exports.combineStateKeys = exports.resolveEffective = void 0;
4
- const crypto_1 = require("crypto");
3
+ exports.computeStateKey = exports.combineStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
5
4
  const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
6
5
  // The state-key engine is shared by the scheduler's hit/miss decision
7
6
  // (`checkCacheState`) and the read-only cache-explain engine, so a prediction
@@ -24,6 +23,7 @@ function isOwnProvable(item, unmatched) {
24
23
  // unmatched ⊆ declared, so this is also false when nothing is declared
25
24
  return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
26
25
  }
26
+ exports.isOwnProvable = isOwnProvable;
27
27
  // Fold a task's own state key together with the state keys of its dependencies.
28
28
  // A change anywhere in the dependency subtree must change the resulting key, so
29
29
  // that a downstream task is invalidated when an upstream source changes — even
@@ -34,9 +34,7 @@ function combineStateKeys(ownKey, depKeys) {
34
34
  }
35
35
  // Sort so the key is independent of dependency declaration order.
36
36
  const sorted = [...depKeys].sort();
37
- return (0, crypto_1.createHash)('md5')
38
- .update([ownKey, ...sorted].join(','))
39
- .digest('hex');
37
+ return (0, get_work_cache_stats_1.stateKeyDigest)([ownKey, ...sorted].join(','));
40
38
  }
41
39
  exports.combineStateKeys = combineStateKeys;
42
40
  // `none` only disables the cache *check* for a task; its sources still influence
@@ -72,7 +70,41 @@ async function computeStateKey(item, defaultCacheMethod, environment, memo = new
72
70
  depKeys.push(effective.stateKey);
73
71
  provable = provable && effective.provable;
74
72
  }
73
+ // what a task that needs a service passed against: the sources of the service and of the
74
+ // tasks it depends on (its definition is in the task id, getWorkServiceCacheIdentity)
75
+ for (const need of item.needs) {
76
+ const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
77
+ depKeys.push(`${need.name}=${effective.stateKey}`);
78
+ }
75
79
  return { stateKey: combineStateKeys(ownKey, depKeys), stats, resolved, provable, unmatched };
76
80
  }
77
81
  exports.computeStateKey = computeStateKey;
82
+ // A needed service's sources folded with the state keys of its dependencies and needs. It
83
+ // does not make the task that needs it unprovable: a dependency without `src` (resetting the
84
+ // service's data, say) is skipped with the service when the task is a cache hit.
85
+ async function resolveNeedStateKey(service, defaultCacheMethod, environment, memo) {
86
+ const memoKey = `need\0${service.name}`;
87
+ const existing = memo.get(memoKey);
88
+ if (existing !== undefined) {
89
+ return existing;
90
+ }
91
+ // services that need each other in a loop fail to plan (checkForLoop): this only has to end
92
+ memo.set(memoKey, { stateKey: 'loop', provable: true });
93
+ const resolved = resolveEffective(service, defaultCacheMethod);
94
+ const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(service, environment);
95
+ const keys = [];
96
+ for (const dep of service.deps) {
97
+ keys.push((await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo)).stateKey);
98
+ }
99
+ for (const need of service.needs) {
100
+ const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
101
+ keys.push(`${need.name}=${effective.stateKey}`);
102
+ }
103
+ const effective = {
104
+ stateKey: combineStateKeys((0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod)), keys),
105
+ provable: true,
106
+ };
107
+ memo.set(memoKey, effective);
108
+ return effective;
109
+ }
78
110
  //# sourceMappingURL=state-key.js.map
@@ -35,7 +35,7 @@ function sandboxPath(path) {
35
35
  * of `run_task` lists; without `shell` there is no `bash`.
36
36
  */
37
37
  function createMcpTools(client, options = {}) {
38
- const [runTask, runLogs] = (0, task_tools_1.createTaskTools)(options.tasks, options.shell || undefined).map((tool) => fromTaskTool(tool, client));
38
+ const [runTask, runLogs, check] = (0, task_tools_1.createTaskTools)(options.tasks, options.shell || undefined).map((tool) => fromTaskTool(tool, client));
39
39
  const bash = {
40
40
  name: 'bash',
41
41
  description: 'Run a shell command on the sandbox and answer its output and exit code. It runs as the shell task of ' +
@@ -196,6 +196,7 @@ function createMcpTools(client, options = {}) {
196
196
  },
197
197
  ...(options.shell === false ? [] : [bash]),
198
198
  runTask,
199
+ check,
199
200
  { ...runLogs, annotations: { ...runLogs.annotations, ...READ_ONLY } },
200
201
  {
201
202
  name: 'changes',
@@ -65,6 +65,7 @@ exports.OPENCODE_TOOLS = [
65
65
  'glob',
66
66
  'run_task',
67
67
  'run_logs',
68
+ 'check',
68
69
  'push',
69
70
  'list',
70
71
  'services_up',
@@ -154,6 +155,7 @@ exports.OPENCODE_V2_TOOLS = [
154
155
  'grep',
155
156
  'run_task',
156
157
  'run_logs',
158
+ 'check',
157
159
  'push',
158
160
  'services_up',
159
161
  'endpoint_get',
@@ -270,7 +272,7 @@ function createOpencodeV2Tools(client, directory, options = {}) {
270
272
  caseSensitive: flag('Match case (default true)'),
271
273
  limit: count(`The maximum number of matching lines (default ${SEARCH_LIMIT})`),
272
274
  }, ['pattern']), same),
273
- ...['run_task', 'run_logs'].map((name) => tool(name, name, { ...(0, task_tools_1.taskToolSchema)(taskTools[name]), additionalProperties: false }, same)),
275
+ ...['run_task', 'run_logs', 'check'].map((name) => tool(name, name, { ...(0, task_tools_1.taskToolSchema)(taskTools[name]), additionalProperties: false }, same)),
274
276
  tool('push', 'push', object({
275
277
  branch: text('The branch to push to, created when it does not exist'),
276
278
  message: text('The commit message'),
@@ -317,7 +319,12 @@ function opencodeTools(client, options = {}) {
317
319
  args: (z) => (0, task_tools_1.taskToolArgs)(task, z),
318
320
  async execute(args, context) {
319
321
  const output = await task.call(await client(), args, context.abort);
320
- return { title: task.name === 'run_task' ? `run ${args.task}` : `log of ${args.run}`, output };
322
+ return {
323
+ title: task.name === 'run_logs'
324
+ ? `log of ${args.run}`
325
+ : `${task.name === 'check' ? 'check' : 'run'} ${args.task}`,
326
+ output,
327
+ };
321
328
  },
322
329
  },
323
330
  ]));
@@ -192,6 +192,21 @@ function hammerkitPiExtension(library, settings = {}) {
192
192
  },
193
193
  });
194
194
  }
195
+ register({
196
+ name: 'check',
197
+ label: 'check',
198
+ description: sandbox_client_1.CHECK_DESCRIPTION,
199
+ promptSnippet: 'Run a task as a check that CI verifies after the push',
200
+ parameters: {
201
+ type: 'object',
202
+ properties: { task: { type: 'string', description: 'The name of the task' } },
203
+ required: ['task'],
204
+ },
205
+ execute: async (_id, params, signal) => {
206
+ const text = await (0, sandbox_client_1.runCheck)(await client(), params.task, signal);
207
+ return { content: [{ type: 'text', text }], details: { task: params.task } };
208
+ },
209
+ });
195
210
  pi.on('session_start', async (_event, ctx) => {
196
211
  try {
197
212
  const sandbox = await client();
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.followRun = exports.submitRun = exports.FINAL_RUN_STATES = exports.oneAtATime = exports.SandboxClient = exports.listPublishedTasks = exports.SandboxClientError = void 0;
3
+ exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.followRun = exports.submitRun = exports.FINAL_RUN_STATES = exports.oneAtATime = exports.SandboxClient = exports.listPublishedTasks = exports.runCheck = exports.CHECK_DESCRIPTION = exports.SandboxClientError = void 0;
4
4
  const path_1 = require("path");
5
5
  const token_source_1 = require("../remote/token-source");
6
6
  // a write carries up to 32 MiB
@@ -16,6 +16,30 @@ class SandboxClientError extends Error {
16
16
  }
17
17
  }
18
18
  exports.SandboxClientError = SandboxClientError;
19
+ // What the agent is told of a check, on each harness.
20
+ exports.CHECK_DESCRIPTION = 'Run a task as a check before you push, such as test or e2e: it runs on a copy of the files a push ' +
21
+ 'would commit (what .gitignore leaves out is not there, so dependencies are installed by the tasks ' +
22
+ 'the build file names), and when it passes, CI verifies it for that commit instead of running it. ' +
23
+ 'Push without changing files afterwards, or the check no longer counts for the files that changed.';
24
+ // A check, run to its end, in a few lines: how it went and the last lines of a failure.
25
+ async function runCheck(client, task, signal) {
26
+ const ran = await client.runTask(task, { check: true, signal });
27
+ const result = ran.result;
28
+ const lines = [
29
+ result?.status === 'succeeded' || result?.status === 'cached'
30
+ ? `check ${task} passed (run ${ran.run.id}${result.status === 'cached' ? ', from the cache' : ''}): CI can verify it for a push of these files`
31
+ : `check ${task} did not pass (run ${ran.run.id}: ${result?.status ?? ran.run.state}${result?.exitCode !== undefined ? `, exit code ${result.exitCode}` : ''})`,
32
+ ];
33
+ const error = result?.error ?? ran.run.error;
34
+ if (error) {
35
+ lines.push(`error: ${error}`);
36
+ }
37
+ if (result && result.status !== 'succeeded' && result.status !== 'cached' && result.lastLog.length > 0) {
38
+ lines.push('last lines:', ...result.lastLog);
39
+ }
40
+ return lines.join('\n');
41
+ }
42
+ exports.runCheck = runCheck;
19
43
  /**
20
44
  * The tasks of a published version, without its services, as `GET /v1/definitions/<hash>`
21
45
  * answers them.
@@ -143,7 +167,7 @@ class SandboxClient {
143
167
  const submitted = await submitRun(this.connection, {
144
168
  definition: this.build.hash,
145
169
  tasks: [task],
146
- source: { type: 'sandbox', id: this.id },
170
+ source: { type: 'sandbox', id: this.id, ...(options.check ? { check: true } : {}) },
147
171
  options: runOptions,
148
172
  });
149
173
  const followed = await followRun(this.connection, submitted, {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskToolArgs = exports.taskToolSchema = exports.createTaskTools = exports.describeTasks = exports.describeRun = exports.readLogHint = exports.readTaskListing = exports.LIST_TASKS_MS = void 0;
4
+ const sandbox_client_1 = require("./sandbox-client");
4
5
  // the most tasks a description lists, and of a task's description or command it shows
5
6
  const MAX_LISTED = 40;
6
7
  const MAX_DESCRIPTION_CHARS = 200;
@@ -100,7 +101,7 @@ function cut(text, max) {
100
101
  return text.length > max ? `${text.slice(0, max)}…` : text;
101
102
  }
102
103
  /**
103
- * `run_task` and `run_logs`, with the tasks the description of `run_task` lists. Without a
104
+ * `run_task`, `run_logs` and `check`, with the tasks the description of `run_task` lists. Without a
104
105
  * listing it names none, as the MCP tools before a version could be read.
105
106
  */
106
107
  function createTaskTools(listing, shell) {
@@ -160,6 +161,16 @@ function createTaskTools(listing, shell) {
160
161
  return lines.join('\n');
161
162
  },
162
163
  },
164
+ {
165
+ name: 'check',
166
+ title: 'Run a check',
167
+ description: sandbox_client_1.CHECK_DESCRIPTION,
168
+ parameters: { task: { type: 'string', description: 'The name of the task' } },
169
+ required: ['task'],
170
+ call(client, args, signal) {
171
+ return (0, sandbox_client_1.runCheck)(client, args.task, signal);
172
+ },
173
+ },
163
174
  ];
164
175
  }
165
176
  exports.createTaskTools = createTaskTools;
@@ -4,9 +4,10 @@ exports.calculateChecksum = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  // Hashes the raw bytes: decoding to text first would map every invalid UTF-8
6
6
  // sequence to U+FFFD, so two different binary files could share a checksum.
7
- // For UTF-8 text the result is unchanged.
7
+ // For UTF-8 text the result is unchanged. sha256, which nobody can make two files share:
8
+ // a cache hit is also what a verified check counts (check-key.ts).
8
9
  async function calculateChecksum(environment, path) {
9
- const hash = (0, crypto_1.createHash)('sha1');
10
+ const hash = (0, crypto_1.createHash)('sha256');
10
11
  for await (const chunk of environment.file.readStream(path)) {
11
12
  hash.update(chunk);
12
13
  }
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCheckKey = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const state_key_1 = require("../executer/scheduler/state-key");
6
+ const get_work_cache_stats_1 = require("./get-work-cache-stats");
7
+ const work_task_cache_description_1 = require("./work-task-cache-description");
8
+ /**
9
+ * What a passed check is recorded under, and what `remote verify` looks it up by: the
10
+ * inputs of the task's cache key, so a cached result counts for the same key it was
11
+ * stored under. Its definition (the services it needs included), the files of its `src`,
12
+ * the keys of its dependencies, and the sources of the services it needs and of the tasks
13
+ * they depend on.
14
+ *
15
+ * It is computed on a checkout before anything ran in it, on both ends, so it also covers
16
+ * the files a commit holds where a dependency generates (the cache leaves those out, as
17
+ * they change once the dependency ran), and it leaves out `.git`, which is no file of a
18
+ * commit. Two checkouts of the same files give the same key wherever they are.
19
+ * Null for a task that can not be proven up to date (no `src` of its own, or none that
20
+ * matches a file, here or in a dependency): its result says nothing about any files.
21
+ */
22
+ async function getCheckKey(item, environment, memo = new Map(), path = new Set()) {
23
+ return memoized(memo, path, `task\0${item.id()}`, (inner) => computeCheckKey(item, environment, memo, inner));
24
+ }
25
+ exports.getCheckKey = getCheckKey;
26
+ async function computeCheckKey(item, environment, memo, path) {
27
+ const { stats, unmatched } = await (0, get_work_cache_stats_1.getWorkItemCheckoutStats)(item, environment);
28
+ if (!(0, state_key_1.isOwnProvable)(item, unmatched)) {
29
+ return null;
30
+ }
31
+ const inputs = await getInputKeys(item, environment, memo, path);
32
+ if (inputs === null) {
33
+ return null;
34
+ }
35
+ return hash({ task: (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item), files: commitFiles(stats), ...inputs });
36
+ }
37
+ // The sources of a needed service, with the keys of what it depends on and needs; its
38
+ // definition is in the description of the task that needs it.
39
+ function getNeedKey(item, environment, memo, path) {
40
+ return memoized(memo, path, `need\0${item.name}`, async (inner) => {
41
+ const inputs = await getInputKeys(item, environment, memo, inner);
42
+ if (inputs === null) {
43
+ return null;
44
+ }
45
+ return hash({ files: commitFiles(await (0, get_work_cache_stats_1.getWorkCacheStats)(item.data, environment)), ...inputs });
46
+ });
47
+ }
48
+ async function getInputKeys(item, environment, memo, path) {
49
+ const deps = [];
50
+ for (const dep of item.deps) {
51
+ const key = await getCheckKey(dep, environment, memo, path);
52
+ if (key === null) {
53
+ return null;
54
+ }
55
+ deps.push(key);
56
+ }
57
+ const needs = [];
58
+ for (const need of item.needs) {
59
+ const key = await getNeedKey(need.service, environment, memo, path);
60
+ if (key === null) {
61
+ return null;
62
+ }
63
+ needs.push(`${need.name}=${key}`);
64
+ }
65
+ return { deps: deps.sort(), needs: needs.sort() };
66
+ }
67
+ // a commit never holds `.git`: what a checkout keeps there differs between a CI job's clone,
68
+ // the server's fetch and a sandbox's copy, which has none
69
+ function commitFiles(stats) {
70
+ return Object.keys(stats.files)
71
+ .filter((path) => !path.split('/').includes('.git'))
72
+ .sort()
73
+ .map((path) => [path, stats.files[path].checksum]);
74
+ }
75
+ function hash(value) {
76
+ return (0, crypto_1.createHash)('sha256').update(JSON.stringify(value)).digest('hex');
77
+ }
78
+ // `path` holds the keys being computed on the way here: one coming back is a loop (services
79
+ // that need each other), which can not be proven and fails to plan anyway. Waiting on its
80
+ // pending promise would never end. The keys are computed one after another, so every pending
81
+ // promise in the memo is on the path.
82
+ function memoized(memo, path, key, compute) {
83
+ if (path.has(key)) {
84
+ return Promise.resolve(null);
85
+ }
86
+ const known = memo.get(key);
87
+ if (known) {
88
+ return known;
89
+ }
90
+ const computed = compute(new Set([...path, key]));
91
+ memo.set(key, computed);
92
+ return computed;
93
+ }
94
+ //# sourceMappingURL=check-key.js.map
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCheckKeys = void 0;
4
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
5
+ const reference_parser_1 = require("../schema/reference-parser");
6
+ const schema_parser_1 = require("../schema/schema-parser");
7
+ const work_scope_parser_1 = require("../schema/work-scope-parser");
8
+ const log_1 = require("../log");
9
+ const check_key_1 = require("./check-key");
10
+ /**
11
+ * The check keys of the tasks of a scope, as both ends compute them: the process of a run on
12
+ * the server before it executes anything, and `remote verify` in a CI job's checkout. So the
13
+ * build file is read the same way on both, as a server reads it (no `.env`, links are files)
14
+ * but placed nowhere: where a server runs a task (a pool, its tool image, the arch of its
15
+ * nodes) is the server's, and a result counts for the server it passed on. The secrets'
16
+ * providers and accounts are the server's too, and no part of a key, so any is taken here.
17
+ */
18
+ async function getCheckKeys(entry, environment, scope) {
19
+ const keyed = { ...environment, hosted: { catalog: anyCatalog(), localTasksInContainers: true } };
20
+ const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(entry, keyed);
21
+ const workTree = (0, work_scope_parser_1.getWorkContext)(await (0, reference_parser_1.parseReferences)(ctx, parsed, keyed), scope, keyed);
22
+ const memo = new Map();
23
+ const keys = {};
24
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
25
+ try {
26
+ const key = await (0, check_key_1.getCheckKey)(task, keyed, memo);
27
+ keys[task.name] = key
28
+ ? { key }
29
+ : {
30
+ key: null,
31
+ reason: 'it, or a task it or a service it needs depends on, declares no src that matches a file',
32
+ };
33
+ }
34
+ catch (e) {
35
+ // a secret whose value is part of the key (`cache: true`) is not known here
36
+ keys[task.name] = { key: null, reason: (0, log_1.getErrorMessage)(e) };
37
+ }
38
+ }
39
+ return keys;
40
+ }
41
+ exports.getCheckKeys = getCheckKeys;
42
+ // Every provider and account name resolves, to a definition nothing runs.
43
+ function anyCatalog() {
44
+ const provider = (name) => ({
45
+ name,
46
+ type: 'command',
47
+ command: [],
48
+ env: {},
49
+ timeoutMs: 0,
50
+ declaredIn: 'the server',
51
+ });
52
+ const account = (name) => ({
53
+ name,
54
+ default: false,
55
+ providers: new Proxy({}, { get: () => ({ env: {} }) }),
56
+ declaredIn: 'the server',
57
+ });
58
+ return {
59
+ providers: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? provider(name) : undefined) }),
60
+ accounts: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? account(name) : undefined) }),
61
+ explicitAccounts: true,
62
+ };
63
+ }
64
+ //# sourceMappingURL=check-keys.js.map
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getStateKey = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
3
+ exports.stateKeyDigest = exports.getStateKey = exports.getWorkItemCheckoutStats = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
4
4
  const calculate_checksum_1 = require("./calculate-checksum");
5
5
  const path_1 = require("path");
6
6
  const crypto_1 = require("crypto");
@@ -20,7 +20,7 @@ async function addWorkCacheStats(result, cwd, path, matcher, context, root, excl
20
20
  if (!matcher(path)) {
21
21
  return 0;
22
22
  }
23
- const checksum = (0, crypto_1.createHash)('sha1')
23
+ const checksum = (0, crypto_1.createHash)('sha256')
24
24
  .update(`link:${await (0, promises_1.readlink)(path)}`)
25
25
  .digest('hex');
26
26
  result.files[(0, path_1.relative)(cwd, path).split(path_1.sep).join(path_1.posix.sep)] = { lastModified: link.mtimeMs, checksum };
@@ -85,6 +85,13 @@ function getWorkItemCacheStats(item, environment) {
85
85
  return collectSourceStats(item.data, environment, dependencyOutputs(item));
86
86
  }
87
87
  exports.getWorkItemCacheStats = getWorkItemCacheStats;
88
+ // The source stats of a task with what its dependencies generate: on a checkout nothing ran
89
+ // in, those are the files a commit holds there, see check-key.ts.
90
+ async function getWorkItemCheckoutStats(item, environment) {
91
+ const { stats } = await collectSourceStats(item.data, environment, []);
92
+ return { stats, unmatched: (await getWorkItemCacheStats(item, environment)).unmatched };
93
+ }
94
+ exports.getWorkItemCheckoutStats = getWorkItemCheckoutStats;
88
95
  function dependencyOutputs(item) {
89
96
  const outputs = new Set();
90
97
  const visited = new Set();
@@ -113,7 +120,14 @@ function getStateKey(stats, cacheMethod) {
113
120
  contents.push(`${key}:${stats.files[key].lastModified}`);
114
121
  }
115
122
  }
116
- return (0, crypto_1.createHash)('md5').update(contents.join(',')).digest('hex');
123
+ return stateKeyDigest(contents.join(','));
117
124
  }
118
125
  exports.getStateKey = getStateKey;
126
+ // A state key is also the value of the `hammerkit.dev/state` label on Kubernetes, which
127
+ // takes at most 63 characters: the sha256 in base 36 has 50.
128
+ function stateKeyDigest(content) {
129
+ const hex = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
130
+ return BigInt(`0x${hex}`).toString(36).padStart(50, '0');
131
+ }
132
+ exports.stateKeyDigest = stateKeyDigest;
119
133
  //# sourceMappingURL=get-work-cache-stats.js.map
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.storedCacheDescription = exports.getWorkTaskCacheDescription = void 0;
3
+ exports.storedCacheDescription = exports.getWorkServiceCacheIdentity = exports.getWorkTaskCacheDescription = void 0;
4
4
  const os_1 = require("os");
5
5
  const crypto_1 = require("crypto");
6
6
  const replace_env_variables_1 = require("../environment/replace-env-variables");
7
7
  const portable_path_1 = require("../planner/utils/portable-path");
8
8
  const work_secret_1 = require("../planner/work-secret");
9
9
  const claim_layout_1 = require("../kubernetes/claim-layout");
10
+ const work_service_1 = require("../planner/work-service");
11
+ const work_service_id_1 = require("../planner/work-service-id");
12
+ const work_cache_id_1 = require("../planner/work-cache-id");
10
13
  // Every path is made project-relative (portablePath) so the description, and the
11
14
  // task id hashed from it, is identical wherever the project is checked out.
12
15
  //
@@ -42,9 +45,54 @@ function getWorkTaskCacheDescription(item) {
42
45
  deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
43
46
  secrets: cachedSecrets.length > 0 ? cachedSecrets : undefined,
44
47
  claimLayout: (0, claim_layout_1.hasPathsEndingAlike)(item) || (0, claim_layout_1.hasOutputsFlatteningAlike)(item) ? 2 : undefined,
48
+ needs: item.needs.length > 0 ? getNeedsIdentities(item) : undefined,
45
49
  };
46
50
  }
47
51
  exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
52
+ // `visiting` holds the services on the way here: services that need each other in a loop
53
+ // fail to plan (checkForLoop), the identity only has to come back to say so
54
+ function getNeedsIdentities(item, visiting = new Set()) {
55
+ return (item.needs
56
+ // an init task needs the service it initializes, whose identity holds the init's id
57
+ .filter((need) => !isInitOf(item, need.service))
58
+ .map((need) => visiting.has(need.service)
59
+ ? `${need.name}=loop`
60
+ : `${need.name}=${getWorkServiceCacheIdentity(need.service, visiting)}`)
61
+ .sort());
62
+ }
63
+ function isInitOf(item, service) {
64
+ return (0, work_service_1.isContainerWorkService)(service.data) && service.data.init?.task.data === item.data;
65
+ }
66
+ // Everything that makes a service what a task needing it talks to: its definition, the ids of
67
+ // the tasks it depends on and of its init task, and the services it needs in turn. The source
68
+ // of those tasks and of the service is in the state key (computeStateKey), like a dependency's.
69
+ // Not the service id, which leaves out what a service runs (getWorkServiceId).
70
+ function getWorkServiceCacheIdentity(item, visiting = new Set()) {
71
+ const service = item.data;
72
+ const definition = (0, work_service_1.isContainerWorkService)(service)
73
+ ? {
74
+ cmd: service.cmd?.cmd ?? null,
75
+ envs: (0, replace_env_variables_1.getEnvironmentVariables)(service.envs),
76
+ healthcheck: service.healthcheck?.cmd.cmd ?? null,
77
+ secrets: service.secrets
78
+ .filter((secret) => secret.cache)
79
+ .map((secret) => `${(0, work_secret_1.getPortableSecretName)(service.projectRoot, secret)}=${secret.digest()}`)
80
+ .sort(),
81
+ // as declared: arguments after -- of a run of the init leave the service as it is
82
+ init: service.init ? (0, work_cache_id_1.getDeclaredWorkTaskId)(service.init.task) : null,
83
+ }
84
+ : { namespace: service.namespace };
85
+ return (0, crypto_1.createHash)('sha256')
86
+ .update(JSON.stringify({
87
+ id: (0, work_service_id_1.getWorkServiceId)(service),
88
+ ...definition,
89
+ ports: service.ports.map((port) => `${port.hostPort ?? ''}:${port.containerPort}`).sort(),
90
+ deps: item.deps.map((dep) => dep.id()).sort(),
91
+ needs: getNeedsIdentities(item, new Set([...visiting, item])),
92
+ }))
93
+ .digest('hex');
94
+ }
95
+ exports.getWorkServiceCacheIdentity = getWorkServiceCacheIdentity;
48
96
  // The form written to disk: description.json in a cache entry (pushed to remote
49
97
  // backends, readable by everyone who can pull) and the local explain record.
50
98
  // Env is where tokens get passed, so each value is stored as a digest; the task
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalizePath = void 0;
3
+ exports.normalizeHostPath = exports.getHomeReference = exports.normalizePath = void 0;
4
4
  const path_1 = require("path");
5
5
  function normalizePath(cwd, pwd, path) {
6
6
  if (path.startsWith('/')) {
@@ -14,4 +14,22 @@ function normalizePath(cwd, pwd, path) {
14
14
  }
15
15
  }
16
16
  exports.normalizePath = normalizePath;
17
+ // What a path of the host starts with to name the home directory: `~` and `$HOME`, and
18
+ // `$PWD`, which in a mount has always meant the home directory, not the project it reads like
19
+ function getHomeReference(path) {
20
+ for (const reference of ['~', '$HOME']) {
21
+ if (path === reference || path.startsWith(`${reference}/`) || path.startsWith(`${reference}\\`)) {
22
+ return reference;
23
+ }
24
+ }
25
+ return path.startsWith('$PWD') ? '$PWD' : null;
26
+ }
27
+ exports.getHomeReference = getHomeReference;
28
+ // A path of the host in a mount: in the home directory when it starts with one of the
29
+ // references above, else in the directory of the build file unless it is absolute
30
+ function normalizeHostPath(cwd, home, path) {
31
+ const reference = getHomeReference(path);
32
+ return reference ? (0, path_1.join)(home, path.substring(reference.length)) : normalizePath(cwd, home, path);
33
+ }
34
+ exports.normalizeHostPath = normalizeHostPath;
17
35
  //# sourceMappingURL=normalize-path.js.map
@@ -7,6 +7,12 @@ const path_1 = require("path");
7
7
  function parseWorkMount(cwd, dir) {
8
8
  const parts = dir.split(':');
9
9
  if (parts.length === 1) {
10
+ const home = (0, normalize_path_1.getHomeReference)(dir);
11
+ if (home === '~' || home === '$HOME') {
12
+ // the container has no such directory: the same path on both sides would be one of the
13
+ // project there (`$PWD` has always been, so it stays)
14
+ throw new Error(`mount ${dir} is in the home directory, name where it goes in the container (${dir}:/path)`);
15
+ }
10
16
  return parseLocalMount(cwd, dir, dir, dir, false);
11
17
  }
12
18
  else if (parts.length === 2) {
@@ -22,7 +28,7 @@ function parseWorkMount(cwd, dir) {
22
28
  exports.parseWorkMount = parseWorkMount;
23
29
  function parseLocalMount(cwd, localPath, containerPath, mount, readOnly) {
24
30
  return {
25
- localPath: (0, normalize_path_1.normalizePath)(cwd, (0, os_1.homedir)(), localPath),
31
+ localPath: (0, normalize_path_1.normalizeHostPath)(cwd, (0, os_1.homedir)(), localPath),
26
32
  containerPath: (0, normalize_path_1.normalizePath)(cwd, cwd, containerPath),
27
33
  isFile: (0, path_1.extname)(localPath).length > 1,
28
34
  mount,
@@ -4,6 +4,7 @@ exports.checkIfContextExists = exports.hasDependencyCycle = exports.hasNeedCycle
4
4
  const read_build_file_1 = require("../parser/read-build-file");
5
5
  const plan_work_tasks_1 = require("./utils/plan-work-tasks");
6
6
  const unreferenced_build_file_envs_1 = require("./unreferenced-build-file-envs");
7
+ const normalize_path_1 = require("./utils/normalize-path");
7
8
  async function* validate(workTree, context) {
8
9
  const cycleItems = [];
9
10
  for (const item of (0, plan_work_tasks_1.iterateWorkServices)(workTree)) {
@@ -16,6 +17,10 @@ async function* validate(workTree, context) {
16
17
  yield { type: 'warn', message: 'missing healthcheck', item: service };
17
18
  }
18
19
  for (const mount of service.mounts) {
20
+ const pwd = getPwdMountWarning(mount);
21
+ if (pwd) {
22
+ yield { type: 'warn', message: pwd, item: service };
23
+ }
19
24
  if (!(await context.file.exists(mount.localPath))) {
20
25
  yield {
21
26
  type: 'warn',
@@ -62,6 +67,12 @@ async function* validate(workTree, context) {
62
67
  item: task,
63
68
  };
64
69
  }
70
+ for (const mount of task.type === 'container-task' ? task.mounts : []) {
71
+ const pwd = getPwdMountWarning(mount);
72
+ if (pwd) {
73
+ yield { type: 'warn', message: pwd, item: task };
74
+ }
75
+ }
65
76
  for (const src of task.src) {
66
77
  if (!(await context.file.exists(src.absolutePath))) {
67
78
  yield {
@@ -146,4 +157,15 @@ async function checkIfContextExists(service, context) {
146
157
  return false;
147
158
  }
148
159
  exports.checkIfContextExists = checkIfContextExists;
160
+ // `$PWD` in a mount is the home directory, which reads like a path of the project
161
+ function getPwdMountWarning(mount) {
162
+ const source = mount.mount.split(':')[0];
163
+ if ((0, normalize_path_1.getHomeReference)(source) !== '$PWD') {
164
+ return null;
165
+ }
166
+ const rest = source.substring('$PWD'.length);
167
+ const home = rest === '' || rest.startsWith('/') || rest.startsWith('\\') ? `~${rest}` : '~/…';
168
+ return (`mount ${source} is ${mount.localPath}: $PWD in a mount is the home directory, not the project. ` +
169
+ `Write ${home} for the home directory, or a relative path for the project`);
170
+ }
149
171
  //# sourceMappingURL=validate.js.map