hammerkit 1.27.0-next.2 → 1.27.0-next.21

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 (103) hide show
  1. package/build.schema.json +27 -0
  2. package/dist/cache/backends/registry-cache-backend.js +2 -2
  3. package/dist/cache/explain.js +17 -0
  4. package/dist/cli.js +1 -0
  5. package/dist/docker/item-network.js +13 -5
  6. package/dist/executer/deadline.js +17 -3
  7. package/dist/executer/docker-service.js +30 -8
  8. package/dist/executer/event-cache.js +20 -2
  9. package/dist/executer/execute-work-task.js +5 -3
  10. package/dist/executer/execution-steps.js +25 -4
  11. package/dist/executer/reset-work-tree.js +4 -0
  12. package/dist/executer/scheduler/enqueue-next.js +7 -0
  13. package/dist/executer/scheduler/state-key.js +42 -5
  14. package/dist/harness/connect.js +20 -6
  15. package/dist/harness/mcp-command.js +22 -6
  16. package/dist/harness/mcp.js +63 -114
  17. package/dist/harness/opencode.js +158 -50
  18. package/dist/harness/pi.js +29 -0
  19. package/dist/harness/sandbox-client.js +207 -65
  20. package/dist/harness/service-tools.js +141 -0
  21. package/dist/harness/task-tools.js +221 -0
  22. package/dist/kubernetes/apply.js +27 -16
  23. package/dist/kubernetes/claim-layout.js +71 -0
  24. package/dist/kubernetes/ensure-http-route.js +1 -1
  25. package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +5 -9
  26. package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +1 -1
  27. package/dist/kubernetes/ensure-kubernetes-service-exists.js +6 -9
  28. package/dist/kubernetes/ensure-persistent-data.js +30 -20
  29. package/dist/kubernetes/get-service-ip.js +2 -2
  30. package/dist/kubernetes/job-secrets.js +33 -19
  31. package/dist/kubernetes/kept-claims.js +22 -10
  32. package/dist/kubernetes/network-policy.js +85 -27
  33. package/dist/kubernetes/remove-persistent-data.js +3 -1
  34. package/dist/kubernetes/resources.js +25 -3
  35. package/dist/kubernetes/restore-kubernetes-data.js +9 -3
  36. package/dist/kubernetes/sandbox-runner.js +626 -0
  37. package/dist/kubernetes/secrets.js +7 -11
  38. package/dist/kubernetes/volumes.js +6 -3
  39. package/dist/network/egress-proxy.js +45 -6
  40. package/dist/optimizer/calculate-checksum.js +3 -2
  41. package/dist/optimizer/check-key.js +94 -0
  42. package/dist/optimizer/check-keys.js +64 -0
  43. package/dist/optimizer/get-work-cache-stats.js +17 -3
  44. package/dist/optimizer/work-task-cache-description.js +51 -1
  45. package/dist/planner/utils/append-work-dependencies.js +10 -5
  46. package/dist/planner/utils/append-work-service.js +1 -0
  47. package/dist/planner/utils/append-work-task.js +15 -2
  48. package/dist/planner/utils/get-work-runtime.js +6 -1
  49. package/dist/planner/utils/normalize-path.js +19 -1
  50. package/dist/planner/utils/parse-work-mount.js +7 -1
  51. package/dist/planner/utils/parse-work-port.js +4 -1
  52. package/dist/planner/utils/task-arguments.js +3 -0
  53. package/dist/planner/validate.js +22 -0
  54. package/dist/planner/work-network.js +3 -1
  55. package/dist/planner/work-runtime-aggregate.js +32 -0
  56. package/dist/planner/work-runtime-docker.js +2 -1
  57. package/dist/planner/work-runtime-kubernetes.js +97 -48
  58. package/dist/planner/work-runtime-local.js +2 -1
  59. package/dist/planner/work-task.js +6 -1
  60. package/dist/program.js +35 -13
  61. package/dist/remote/annotations.js +13 -1
  62. package/dist/remote/device-login.js +7 -2
  63. package/dist/remote/host-config.js +64 -3
  64. package/dist/remote/remote-commands.js +41 -9
  65. package/dist/remote/remote-work-commands.js +246 -1
  66. package/dist/remote/server-client.js +15 -2
  67. package/dist/remote/token-source.js +64 -26
  68. package/dist/run-program.js +9 -0
  69. package/dist/schema/build-file-base-task-schema.js +5 -1
  70. package/dist/schema/build-file-container-service-schema.js +2 -0
  71. package/dist/schema/build-file-include-schema.js +10 -4
  72. package/dist/schema/reference-parser.js +9 -1
  73. package/dist/schema/schema-parser.js +36 -4
  74. package/dist/schema/work-scope-parser.js +33 -10
  75. package/dist/secrets/github-app-token-command.js +3 -1
  76. package/dist/server/accounts-api.js +3 -1
  77. package/dist/server/approvals-api.js +1 -1
  78. package/dist/server/checks-api.js +83 -0
  79. package/dist/server/checks.js +116 -0
  80. package/dist/server/definitions-api.js +4 -2
  81. package/dist/server/endpoints.js +612 -0
  82. package/dist/server/executor.js +56 -5
  83. package/dist/server/kubernetes-backend.js +303 -64
  84. package/dist/server/kubernetes-sandbox-storage.js +48 -3
  85. package/dist/server/oauth.js +12 -1
  86. package/dist/server/plan.js +126 -62
  87. package/dist/server/policy.js +32 -2
  88. package/dist/server/repo-access.js +5 -0
  89. package/dist/server/run-plan.js +50 -19
  90. package/dist/server/run-request.js +64 -3
  91. package/dist/server/run-worker.js +146 -10
  92. package/dist/server/runs.js +347 -27
  93. package/dist/server/sandbox-pod.js +28 -0
  94. package/dist/server/sandbox-storage.js +23 -0
  95. package/dist/server/sandboxes-api.js +10 -4
  96. package/dist/server/sandboxes.js +8 -0
  97. package/dist/server/server-command.js +39 -20
  98. package/dist/server/server-config.js +213 -14
  99. package/dist/server/server.js +268 -15
  100. package/dist/server/service-runs.js +82 -0
  101. package/dist/utils/abort-on-signals.js +46 -6
  102. package/dist/utils/tar.js +36 -0
  103. package/package.json +1 -1
@@ -7,6 +7,8 @@ exports.runProgram = void 0;
7
7
  const program_1 = require("./program");
8
8
  const parse_error_1 = require("./schema/parse-error");
9
9
  const colors_1 = __importDefault(require("colors"));
10
+ const commander_1 = require("commander");
11
+ const log_1 = require("./log");
10
12
  function runProgram(env, argv, exitOverride) {
11
13
  return (0, program_1.getProgram)(env, argv)
12
14
  .then(({ program, args }) => {
@@ -18,8 +20,15 @@ function runProgram(env, argv, exitOverride) {
18
20
  for (const error of err.zod.errors) {
19
21
  env.stderr.write(` ${colors_1.default.underline(colors_1.default.blue(`error`))} at ${colors_1.default.gray(error.path.join('.'))} ${error.message} ${colors_1.default.gray(error.code)}\n`);
20
22
  }
23
+ // printed, but still a failure: CI must not pass on a build file that doesn't parse
24
+ throw err;
21
25
  }
22
26
  else {
27
+ // what commander reports (program.error, a wrong option) is printed already; anything
28
+ // else, such as a build file that is no YAML, would otherwise end the process silently
29
+ if (!(err instanceof commander_1.CommanderError)) {
30
+ env.stderr.write(`${colors_1.default.red('error')}: ${(0, log_1.getErrorMessage)(err)}\n`);
31
+ }
23
32
  throw err;
24
33
  }
25
34
  });
@@ -40,8 +40,12 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
40
40
  account: (0, zod_1.string)().optional(),
41
41
  // CPU and memory limits of the container; a local task is not limited
42
42
  resources: resources_schema_1.resourcesSchema.optional(),
43
- // what the container reaches besides its needs; a local task can't be limited
43
+ // what the container reaches besides its needs; a local task can be limited only where
44
+ // it runs in a container, on a server's cluster
44
45
  network: network_schema_1.networkSchema.optional(),
46
+ // whether its last command takes the arguments after `--`: false refuses them, true
47
+ // says it is meant to, which a server with `runs.args: declared` requires
48
+ args: (0, zod_1.boolean)().optional(),
45
49
  // Name of the server (see `hammerkit remote`) the task has to run on; defaults
46
50
  // to the build file's `runner`. Never a local fallback when it is unknown.
47
51
  runner: runner_schema_1.runnerSchema.optional(),
@@ -45,6 +45,8 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
45
45
  volumeSize: size_schema_1.sizeSchema.describe('size of its claim on Kubernetes, e.g. 10Gi').optional(),
46
46
  // what the service reaches besides its needs; tasks needing it always reach it
47
47
  network: network_schema_1.networkSchema.optional(),
48
+ // whether its `cmd` takes the arguments after `--` of `up`: false refuses them
49
+ args: (0, zod_1.boolean)().optional(),
48
50
  })
49
51
  .strict()
50
52
  .describe('container service');
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileGitSourceSchema = void 0;
3
+ exports.isGitSource = exports.buildFileIncludeSchema = exports.buildFileLocalSourceSchema = exports.buildFileGitSourceSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const envs_schema_1 = require("./envs-schema");
6
6
  // A repo URL or ref starting with `-` would be read by git as an option
@@ -22,10 +22,16 @@ exports.buildFileGitSourceSchema = (0, zod_1.object)({
22
22
  // overrides for the top-level `envs` the included file declares as its inputs
23
23
  with: envs_schema_1.envsSchema.optional(),
24
24
  }).strict();
25
- // A string is a local path; an object is a git source (ADR-0004).
26
- exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileGitSourceSchema]);
25
+ // A local path with values for the inputs of the file it names, like `with` of a git
26
+ // source. A value may pass on an `envs` value of the including file as `${NAME}`.
27
+ exports.buildFileLocalSourceSchema = (0, zod_1.object)({
28
+ path: (0, zod_1.string)().min(1).describe('Build file or directory, relative to the including file'),
29
+ with: envs_schema_1.envsSchema.optional(),
30
+ }).strict();
31
+ // A string is a local path; an object is a local path with inputs, or a git source (ADR-0004).
32
+ exports.buildFileIncludeSchema = (0, zod_1.union)([(0, zod_1.string)(), exports.buildFileLocalSourceSchema, exports.buildFileGitSourceSchema]);
27
33
  function isGitSource(include) {
28
- return typeof include !== 'string';
34
+ return typeof include !== 'string' && 'git' in include;
29
35
  }
30
36
  exports.isGitSource = isGitSource;
31
37
  //# sourceMappingURL=build-file-include-schema.js.map
@@ -129,6 +129,7 @@ function applySchemaExtension(reference, task) {
129
129
  task.schema.timeout = task.schema.timeout ?? extend.schema.timeout;
130
130
  task.schema.runner = task.schema.runner ?? extend.schema.runner;
131
131
  task.schema.network = task.schema.network ?? extend.schema.network;
132
+ task.schema.args = task.schema.args ?? extend.schema.args;
132
133
  if (!reset.has('cmds'))
133
134
  task.schema.cmds = extendArray(task.schema.cmds, extend.schema.cmds);
134
135
  if (!reset.has('src'))
@@ -209,7 +210,9 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
209
210
  for (const needName of schema.needs || []) {
210
211
  if (typeof needName === 'string') {
211
212
  taskOrService.needs.push({
212
- relativeName: needName,
213
+ // the host name it is reached by: a service of an include, `services:postgres`, as
214
+ // `services-postgres`, since Docker and DNS take no colon
215
+ relativeName: needName.replace(/:/g, '-'),
213
216
  service: findService(reference, scope, needName),
214
217
  });
215
218
  }
@@ -220,6 +223,11 @@ function resolveNeeds(reference, taskOrService, scope, schema) {
220
223
  });
221
224
  }
222
225
  }
226
+ const names = taskOrService.needs.map((need) => need.relativeName);
227
+ const twice = names.find((name, index) => names.indexOf(name) !== index);
228
+ if (twice) {
229
+ throw new Error(`${taskOrService.relativeName} needs two services as ${twice}, give one another name with name:`);
230
+ }
223
231
  }
224
232
  function findTask(ctx, scope, name, type) {
225
233
  const ref = (0, split_name_1.splitName)(name);
@@ -89,13 +89,45 @@ async function locateInclude(ctx, scope, include, base, environment) {
89
89
  if (!(0, resolve_git_source_1.isWithin)(remote.root, path)) {
90
90
  throw new Error(`${include.path} in ${include.git} points outside the repository`);
91
91
  }
92
- return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: include.with };
92
+ return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: passEnvs(include.with, scope) };
93
93
  }
94
- const path = (0, path_1.join)(base, include);
94
+ const localPath = typeof include === 'string' ? include : include.path;
95
+ const path = (0, path_1.join)(base, localPath);
95
96
  if (scope.remote && !(0, resolve_git_source_1.isWithin)(scope.remote.root, path)) {
96
- throw new Error(`${include} in ${scope.fileName} points outside ${scope.remote.git}`);
97
+ throw new Error(`${localPath} in ${scope.fileName} points outside ${scope.remote.git}`);
97
98
  }
98
- return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote };
99
+ const inputs = typeof include === 'string' ? undefined : passEnvs(include.with, scope);
100
+ return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote, inputs };
101
+ }
102
+ // `${NAME}` (or `${NAME:-default}`) in a `with` value reads as it does in the including
103
+ // file: the environment or a `.env` file first, then that file's own `envs` value. So one
104
+ // file holds a value such as NODE_VERSION for all it includes, and CI can still set it. A
105
+ // name the file does not declare is left to the included file, which reads it the same way.
106
+ function passEnvs(inputs, scope) {
107
+ if (!inputs) {
108
+ return undefined;
109
+ }
110
+ const envs = scope.schema.envs ?? {};
111
+ const result = {};
112
+ for (const [key, value] of Object.entries(inputs)) {
113
+ result[key] =
114
+ typeof value === 'string'
115
+ ? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => name in envs ? passEnv(name, `${envs[name]}`, fallback) : match)
116
+ : value;
117
+ }
118
+ return result;
119
+ }
120
+ // The including file's value as an interpolation the included file resolves in the same
121
+ // order. A value built from other variables is passed on as it is written.
122
+ function passEnv(name, value, fallback) {
123
+ const reference = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
124
+ if (reference) {
125
+ return fallback === undefined ? `\${${reference[1]}}` : `\${${reference[1]}:-${fallback}}`;
126
+ }
127
+ if (value.includes('$') || value.includes('}')) {
128
+ return value;
129
+ }
130
+ return `\${${name}:-${value}}`;
99
131
  }
100
132
  // `includes pull` fetches each source once, however often the build files name it.
101
133
  async function resolveRemote(ctx, source, environment) {
@@ -47,19 +47,27 @@ function getWorkContext(context, scope, environment) {
47
47
  // its init task's need on it, which narrowing follows
48
48
  const tasks = Object.values(context.tasks).map((task) => (0, append_work_task_1.appendWorkTask)(workTree, task.cwd, task, environment, context));
49
49
  const services = Object.values(context.services).map((service) => (0, append_work_service_1.appendWorkService)(workTree, service, environment, context));
50
+ const apply = { servicesAlone: !(0, work_scope_1.isContextTaskFilter)(scope) && !!scope.names && !!scope.stop };
50
51
  const selectedTasks = [];
51
52
  const selectedServices = [];
52
53
  for (const item of tasks) {
53
54
  if (matchesTaskWorkScope(item, scope)) {
54
55
  requested.push(item.name);
55
56
  selectedTasks.push(item.data);
56
- applyTask(filteredWorkTree, item);
57
+ applyTask(filteredWorkTree, item, apply);
57
58
  }
58
59
  }
59
60
  for (const item of services) {
60
61
  if (matchesServiceWorkScope(item.data, scope)) {
61
62
  selectedServices.push(item.data);
62
- applyService(filteredWorkTree, item);
63
+ applyService(filteredWorkTree, item, apply);
64
+ }
65
+ }
66
+ if (!(0, work_scope_1.isContextTaskFilter)(scope) && scope.names) {
67
+ const known = new Set([...tasks.map((task) => task.name), ...services.map((service) => service.data.name)]);
68
+ const unknown = scope.names.filter((name) => !known.has(name));
69
+ if (unknown.length > 0) {
70
+ throw new Error(`no task or service named ${unknown.join(', ')}, see hammerkit ls`);
63
71
  }
64
72
  }
65
73
  if (scope.args) {
@@ -86,43 +94,58 @@ function keepRequirersInScope(workTree) {
86
94
  }
87
95
  }
88
96
  }
89
- function applyTask(workTree, item) {
97
+ function applyTask(workTree, item, options) {
90
98
  if (workTree.tasks[item.name]) {
91
99
  return;
92
100
  }
93
101
  workTree.tasks[item.name] = item;
94
- applyNeedsAndDeps(workTree, item);
102
+ applyNeedsAndDeps(workTree, item, options);
95
103
  }
96
- function applyService(workTree, need) {
104
+ function applyService(workTree, need, options) {
97
105
  const item = (0, append_work_service_1.getPlannedService)(need);
98
106
  if (workTree.services[item.name]) {
99
107
  return;
100
108
  }
101
109
  workTree.services[item.name] = item;
102
- applyNeedsAndDeps(workTree, item);
110
+ if (options.servicesAlone) {
111
+ return;
112
+ }
113
+ applyNeedsAndDeps(workTree, item, options);
103
114
  // a service in the run brings its init task, which runs once it is healthy
104
115
  if (item.data.type === 'container-service' && item.data.init) {
105
- applyTask(workTree, item.data.init.task);
116
+ applyTask(workTree, item.data.init.task, options);
106
117
  }
107
118
  }
108
- function applyNeedsAndDeps(workTree, item) {
119
+ function applyNeedsAndDeps(workTree, item, options) {
109
120
  for (const dep of item.deps) {
110
- applyTask(workTree, dep);
121
+ applyTask(workTree, dep, options);
111
122
  }
112
123
  for (const need of item.needs) {
113
- applyService(workTree, need.service);
124
+ applyService(workTree, need.service, options);
114
125
  }
115
126
  }
116
127
  function matchesTaskWorkScope(task, scope) {
117
128
  if ((0, work_scope_1.isContextTaskFilter)(scope)) {
118
129
  return task.name === scope.taskName;
119
130
  }
131
+ if (scope.serviceNames) {
132
+ return false;
133
+ }
134
+ if (scope.names) {
135
+ return scope.names.includes(task.name);
136
+ }
120
137
  return (0, label_values_1.appliesToLabels)(task.data.labels, scope);
121
138
  }
122
139
  function matchesServiceWorkScope(service, scope) {
123
140
  if ((0, work_scope_1.isContextTaskFilter)(scope)) {
124
141
  return false;
125
142
  }
143
+ if (scope.serviceNames) {
144
+ return scope.serviceNames.includes(service.name);
145
+ }
146
+ if (scope.names) {
147
+ return scope.names.includes(service.name);
148
+ }
126
149
  return (0, label_values_1.appliesToLabels)(service.labels, scope);
127
150
  }
128
151
  //# sourceMappingURL=work-scope-parser.js.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerGithubAppTokenCommand = void 0;
4
+ const commander_1 = require("commander");
4
5
  const log_1 = require("../log");
5
6
  const github_app_token_1 = require("./github-app-token");
6
7
  // `hammerkit github-app-token <owner/repo>`: a secret provider command for the token a
@@ -18,7 +19,8 @@ function registerGithubAppTokenCommand(program, environment) {
18
19
  }
19
20
  catch (e) {
20
21
  environment.stderr.write(`github-app-token: ${(0, log_1.getErrorMessage)(e)}\n`);
21
- throw e;
22
+ // said already: the CLI does not print it again
23
+ throw new commander_1.CommanderError(1, 'hammerkit.githubAppToken', (0, log_1.getErrorMessage)(e));
22
24
  }
23
25
  });
24
26
  }
@@ -6,7 +6,7 @@ const policy_1 = require("./policy");
6
6
  /**
7
7
  * Who the caller is and what they may do:
8
8
  *
9
- * GET /v1/me identity, accounts with their level, repositories, the builds level and the sandbox grants
9
+ * GET /v1/me identity, accounts with their level, repositories, the builds level the sandbox and endpoint grants
10
10
  * GET /v1/accounts the accounts the caller has a level on, with what the operator says about them
11
11
  */
12
12
  async function handleAccounts(config, log, principal, req, res, url) {
@@ -31,7 +31,9 @@ async function handleAccounts(config, log, principal, req, res, url) {
31
31
  accounts,
32
32
  repos: grant.repos,
33
33
  builds: grant.builds,
34
+ checks: grant.checks || grant.builds !== null ? 'read' : null,
34
35
  sandboxes: grant.sandboxes,
36
+ endpoints: grant.endpoints,
35
37
  });
36
38
  return true;
37
39
  }
@@ -13,7 +13,7 @@ function toApprovalView(run) {
13
13
  return {
14
14
  id: run.id,
15
15
  state: run.state,
16
- requestedBy: run.owner,
16
+ requestedBy: { iss: run.owner.iss, sub: run.owner.sub },
17
17
  createdAt: run.createdAt,
18
18
  definition: run.definition,
19
19
  tasks: run.tasks,
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleChecks = exports.MAX_LOOKUP_KEYS = void 0;
4
+ const zod_1 = require("zod");
5
+ const http_1 = require("./http");
6
+ const policy_1 = require("./policy");
7
+ const checks_1 = require("./checks");
8
+ const repo_1 = require("./repo");
9
+ // the tasks one lookup may ask about
10
+ exports.MAX_LOOKUP_KEYS = 100;
11
+ const lookupSchema = zod_1.z
12
+ .object({
13
+ repo: zod_1.z.string().transform((value, ctx) => {
14
+ const repo = (0, repo_1.normalizeRepo)(value);
15
+ if (!repo) {
16
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, message: 'expected a repository like github.com/corp/payments' });
17
+ return zod_1.z.NEVER;
18
+ }
19
+ return repo;
20
+ }),
21
+ keys: zod_1.z
22
+ .array(zod_1.z
23
+ .object({
24
+ task: zod_1.z.string().min(1).max(200),
25
+ key: zod_1.z.string().regex(checks_1.CHECK_KEY_PATTERN, 'expected a check key, 64 hex digits'),
26
+ })
27
+ .strict())
28
+ .min(1)
29
+ .max(exports.MAX_LOOKUP_KEYS),
30
+ })
31
+ .strict();
32
+ /**
33
+ * What runs passed, for CI that verifies instead of running (`hammerkit remote verify`):
34
+ *
35
+ * POST /v1/checks/lookup { repo, keys: [{ task, key }] } the newest passed run per key
36
+ *
37
+ * The caller computed the keys from its own checkout (check-key.ts); a key nothing passed
38
+ * for, or whose version of the build file was yanked since, is answered with none.
39
+ */
40
+ async function handleChecks(config, checks, definitions, log, principal, req, res, url) {
41
+ if (url.pathname !== '/v1/checks/lookup') {
42
+ return false;
43
+ }
44
+ if (req.method !== 'POST') {
45
+ (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
46
+ return true;
47
+ }
48
+ const parsed = lookupSchema.safeParse(await (0, http_1.readJson)(req));
49
+ if (!parsed.success) {
50
+ const issue = parsed.error.issues[0];
51
+ (0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
52
+ return true;
53
+ }
54
+ const { repo, keys } = parsed.data;
55
+ const grant = (0, policy_1.getGrant)(config.entitlements, principal);
56
+ if (!(0, policy_1.mayReadChecks)(grant, repo)) {
57
+ log(`refuse checks iss=${principal.iss} sub=${principal.sub} repo=${repo}`);
58
+ (0, http_1.send)(res, 403, { error: `you may not read the checks of ${repo}` });
59
+ return true;
60
+ }
61
+ const answers = await Promise.all(keys.map(async ({ task, key }) => {
62
+ const record = await checks.find(repo, key);
63
+ const yanked = record ? definitions.find(record.definition)?.yanked : undefined;
64
+ if (!record || yanked) {
65
+ return {
66
+ task,
67
+ key,
68
+ passed: null,
69
+ ...(yanked ? { reason: `it passed with ${record?.definition}, which was yanked since` } : {}),
70
+ };
71
+ }
72
+ const { repo: _repo, key: _key, accounts: _accounts, ...view } = record;
73
+ void _repo;
74
+ void _key;
75
+ void _accounts;
76
+ return { task, key, passed: view };
77
+ }));
78
+ log(`checks iss=${principal.iss} sub=${principal.sub} repo=${repo} passed=${answers.filter((a) => a.passed).length}/${answers.length}`);
79
+ (0, http_1.send)(res, 200, { checks: answers });
80
+ return true;
81
+ }
82
+ exports.handleChecks = handleChecks;
83
+ //# sourceMappingURL=checks-api.js.map
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CheckStore = exports.getCheckRecords = exports.isCheckRun = exports.CHECK_KEY_PATTERN = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const promises_1 = require("fs/promises");
6
+ const path_1 = require("path");
7
+ exports.CHECK_KEY_PATTERN = /^[0-9a-f]{64}$/;
8
+ // The runs whose results are checks: those of a commit the server checked out, and those
9
+ // on a copy of the files of a sandbox, which hold nothing a commit of them would not. A run
10
+ // with arguments ran something other than the task, and is no check of it.
11
+ function isCheckRun(run) {
12
+ // a run of services keeps them up, it passes nothing
13
+ if ((run.options.args ?? []).length > 0 || run.up) {
14
+ return false;
15
+ }
16
+ return run.source.type === 'ref' || (run.source.type === 'sandbox' && run.source.check === true);
17
+ }
18
+ exports.isCheckRun = isCheckRun;
19
+ // What a run that ended passed, with the keys its executor reported before the tasks ran.
20
+ function getCheckRecords(run, keys) {
21
+ if (!isCheckRun(run) || run.source.type === 'upload') {
22
+ return [];
23
+ }
24
+ const source = run.source.type === 'ref'
25
+ ? { type: 'ref', ref: run.source.ref, commit: run.source.commit }
26
+ : { type: 'sandbox', sandbox: run.source.id, commit: run.source.commit, tree: run.source.tree };
27
+ const records = [];
28
+ for (const [task, result] of Object.entries(run.results ?? {})) {
29
+ const key = keys[task];
30
+ if (!key || !exports.CHECK_KEY_PATTERN.test(key) || (result.status !== 'succeeded' && result.status !== 'cached')) {
31
+ continue;
32
+ }
33
+ records.push({
34
+ repo: run.source.repo,
35
+ key,
36
+ task,
37
+ status: result.status,
38
+ run: run.id,
39
+ by: { iss: run.owner.iss, sub: run.owner.sub },
40
+ source,
41
+ definition: run.definition,
42
+ accounts: [...run.accounts],
43
+ finishedAt: result.finishedAt ?? run.finishedAt ?? new Date().toISOString(),
44
+ ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
45
+ });
46
+ }
47
+ return records;
48
+ }
49
+ exports.getCheckRecords = getCheckRecords;
50
+ /**
51
+ * One file per repository and key, the newest run's: `<directory>/<repo hash>/<key>.json`,
52
+ * written to a temporary file and renamed. A record older than `keep` is not found and is
53
+ * removed by `sweep`.
54
+ */
55
+ class CheckStore {
56
+ constructor(directory, options) {
57
+ this.directory = directory;
58
+ this.options = options;
59
+ this.now = options.now ?? Date.now;
60
+ }
61
+ async save(records) {
62
+ for (const record of records) {
63
+ const folder = (0, path_1.join)(this.directory, repoFolder(record.repo));
64
+ await (0, promises_1.mkdir)(folder, { recursive: true, mode: 0o700 });
65
+ const file = (0, path_1.join)(folder, `${record.key}.json`);
66
+ const temporary = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
67
+ await (0, promises_1.writeFile)(temporary, JSON.stringify(record), { mode: 0o600 });
68
+ await (0, promises_1.rename)(temporary, file);
69
+ }
70
+ }
71
+ async find(repo, key) {
72
+ if (!exports.CHECK_KEY_PATTERN.test(key)) {
73
+ return undefined;
74
+ }
75
+ let record;
76
+ try {
77
+ record = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(this.directory, repoFolder(repo), `${key}.json`), 'utf8'));
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ if (record.repo !== repo || record.key !== key || this.expired(record)) {
83
+ return undefined;
84
+ }
85
+ return record;
86
+ }
87
+ // Removes the records that are older than `keep`.
88
+ async sweep() {
89
+ let folders;
90
+ try {
91
+ folders = await (0, promises_1.readdir)(this.directory);
92
+ }
93
+ catch {
94
+ return;
95
+ }
96
+ for (const folder of folders) {
97
+ const path = (0, path_1.join)(this.directory, folder);
98
+ for (const name of await (0, promises_1.readdir)(path).catch(() => [])) {
99
+ const file = (0, path_1.join)(path, name);
100
+ const stats = await (0, promises_1.stat)(file).catch(() => undefined);
101
+ if (stats && this.now() - stats.mtimeMs > this.options.keep) {
102
+ await (0, promises_1.rm)(file, { force: true });
103
+ }
104
+ }
105
+ }
106
+ }
107
+ expired(record) {
108
+ const at = Date.parse(record.finishedAt);
109
+ return !Number.isFinite(at) || this.now() - at > this.options.keep;
110
+ }
111
+ }
112
+ exports.CheckStore = CheckStore;
113
+ function repoFolder(repo) {
114
+ return (0, crypto_1.createHash)('sha256').update(repo).digest('hex').slice(0, 32);
115
+ }
116
+ //# sourceMappingURL=checks.js.map
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
5
5
  const bundle_1 = require("../remote/bundle");
6
6
  const definitions_1 = require("./definitions");
7
7
  const http_1 = require("./http");
8
+ const run_plan_1 = require("./run-plan");
8
9
  const oauth_1 = require("./oauth");
9
10
  const policy_1 = require("./policy");
10
11
  const proposals_1 = require("./proposals");
@@ -19,7 +20,8 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
19
20
  * `annotations`, which are not part of the hash
20
21
  * GET /v1/definitions the versions, newest first; `?<key>=<value>`
21
22
  * keeps the ones with those annotations
22
- * GET /v1/definitions/<hash> one version
23
+ * GET /v1/definitions/<hash> one version, with `arguments`: per task null when
24
+ * a run of it may pass arguments, else why not
23
25
  * GET /v1/definitions/<hash>/files its files
24
26
  * POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
25
27
  * POST /v1/definitions/<hash>/unyank let it go on
@@ -72,7 +74,7 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
72
74
  bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
73
75
  }
74
76
  else {
75
- (0, http_1.send)(res, 200, found);
77
+ (0, http_1.send)(res, 200, found.plan ? { ...found, arguments: (0, run_plan_1.describeArguments)(found.plan, config.runs) } : found);
76
78
  }
77
79
  return true;
78
80
  }