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

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
@@ -6,6 +6,7 @@ const read_build_file_1 = require("../parser/read-build-file");
6
6
  const path_1 = require("path");
7
7
  const default_build_file_1 = require("../parser/default-build-file");
8
8
  const parse_error_1 = require("./parse-error");
9
+ const removed_properties_1 = require("./removed-properties");
9
10
  const build_file_include_schema_1 = require("./build-file-include-schema");
10
11
  const resolve_git_source_1 = require("../includes/resolve-git-source");
11
12
  async function createParseContext(fileName, environment, options) {
@@ -13,60 +14,80 @@ async function createParseContext(fileName, environment, options) {
13
14
  files: {},
14
15
  refreshed: options?.refresh ? new Map() : undefined,
15
16
  directory: options?.directory,
17
+ checkGitSource: options?.checkGitSource,
16
18
  };
17
19
  const scope = await appendBuildFile(workDirectory(ctx, (0, path_1.dirname)(fileName)), ctx, environment, fileName, []);
18
20
  return { ctx, scope };
19
21
  }
20
22
  exports.createParseContext = createParseContext;
21
- async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs) {
23
+ async function appendBuildFile(cwd, ctx, environment, fileName, namePrefix, remote, inputs,
24
+ // on a server: the file is included with `publish: false`, or below such an include
25
+ leftOut = false) {
22
26
  const relativeName = namePrefix.join(':');
23
- // the same file included with other inputs is another set of tasks
24
- const key = inputs ? `${cwd}:${fileName}:${JSON.stringify(inputs)}` : `${cwd}:${fileName}`;
27
+ // the same file included with other inputs is another set of tasks, and on a server a
28
+ // file left out is another one than the same file published (its git sources differ)
29
+ const key = `${cwd}:${fileName}${inputs ? `:${JSON.stringify(inputs)}` : ''}${leftOut ? ':left-out' : ''}`;
25
30
  if (ctx.files[key]) {
26
31
  return ctx.files[key];
27
32
  }
28
33
  const input = await (0, read_build_file_1.read)(fileName, environment);
34
+ (0, removed_properties_1.assertNoRemovedProperties)(input, fileName);
29
35
  const result = await build_file_schema_1.buildFileSchema.safeParseAsync(input);
30
- if (result.success) {
31
- const scope = {
32
- fileName,
33
- cwd,
34
- schema: inputs ? applyInputs(result.data, inputs, namePrefix, fileName) : result.data,
35
- namePrefix: relativeName,
36
- remote,
37
- inputs,
38
- references: {},
39
- };
40
- ctx.files[key] = scope;
41
- if (scope.schema.references) {
42
- for (const referenceName of Object.keys(scope.schema.references)) {
43
- const target = await locateInclude(ctx, scope, scope.schema.references[referenceName],
44
- // a remote file's paths resolve within its repository
45
- remote ? (0, path_1.dirname)(scope.fileName) : readDirectory(ctx, scope.cwd), environment);
46
- const referenceScope = await appendBuildFile(workDirectory(ctx, (0, path_1.dirname)(target.fileName)), ctx, environment, target.fileName, [...namePrefix, referenceName], target.remote, target.inputs);
47
- if (scope.references[referenceName]) {
48
- throw new Error(referenceName + ' already exists');
49
- }
50
- scope.references[referenceName] = { type: 'reference', scope: referenceScope };
36
+ if (!result.success) {
37
+ throw new parse_error_1.ParseError(result.error, fileName);
38
+ }
39
+ const scope = {
40
+ fileName,
41
+ cwd,
42
+ schema: inputs ? applyInputs(result.data, inputs, namePrefix, fileName) : result.data,
43
+ namePrefix: relativeName,
44
+ remote,
45
+ inputs,
46
+ references: {},
47
+ };
48
+ ctx.files[key] = scope;
49
+ for (const block of ['references', 'includes']) {
50
+ for (const [name, include] of Object.entries(scope.schema[block] ?? {})) {
51
+ if (scope.references[name]) {
52
+ throw new Error(name + ' already exists');
51
53
  }
52
- }
53
- if (scope.schema.includes) {
54
- for (const includeName of Object.keys(scope.schema.includes)) {
55
- const target = await locateInclude(ctx, scope, scope.schema.includes[includeName], (0, path_1.dirname)(scope.fileName), environment);
56
- const includeScope = await appendBuildFile(cwd, ctx, environment, target.fileName, [...namePrefix, includeName], target.remote, target.inputs);
57
- if (scope.references[includeName]) {
58
- throw new Error(includeName + ' already exists');
59
- }
60
- scope.references[includeName] = { type: 'include', scope: includeScope };
54
+ const publish = (0, build_file_include_schema_1.isPublished)(include);
55
+ const type = block === 'references' ? 'reference' : 'include';
56
+ // a server does not fetch what it leaves out: it doesn't run, and isn't checked
57
+ const leaveOut = !!environment.hosted && (leftOut || !publish);
58
+ if ((0, build_file_include_schema_1.isGitSource)(include) && leaveOut) {
59
+ scope.references[name] = { type, publish, scope: notFetched(cwd, [...namePrefix, name], include) };
60
+ continue;
61
61
  }
62
+ if ((0, build_file_include_schema_1.isGitSource)(include)) {
63
+ ctx.checkGitSource?.(include, scope, block, name);
64
+ }
65
+ const target = await locateInclude(ctx, scope, include, block === 'references'
66
+ ? // a remote file's paths resolve within its repository
67
+ remote
68
+ ? (0, path_1.dirname)(scope.fileName)
69
+ : readDirectory(ctx, scope.cwd)
70
+ : (0, path_1.dirname)(scope.fileName), environment);
71
+ const targetScope = await appendBuildFile(
72
+ // a reference works in its own directory, an include in the one of the including file
73
+ block === 'references' ? workDirectory(ctx, (0, path_1.dirname)(target.fileName)) : cwd, ctx, environment, target.fileName, [...namePrefix, name], target.remote, target.inputs, leaveOut);
74
+ scope.references[name] = { type, scope: targetScope, publish };
62
75
  }
63
- return scope;
64
- }
65
- else {
66
- throw new parse_error_1.ParseError(result.error, fileName);
67
76
  }
77
+ return scope;
68
78
  }
69
79
  exports.appendBuildFile = appendBuildFile;
80
+ // The scope of a git source a server does not fetch, see ParseScope.notFetched
81
+ function notFetched(cwd, namePrefix, include) {
82
+ return {
83
+ fileName: `${include.git}${include.path ? ` ${include.path}` : ''}`,
84
+ cwd,
85
+ schema: {},
86
+ namePrefix: namePrefix.join(':'),
87
+ references: {},
88
+ notFetched: true,
89
+ };
90
+ }
70
91
  // The directory a build file works in, for the directory it was read from: the same
71
92
  // one, unless the files were read from elsewhere (see BuildFileDirectory).
72
93
  function workDirectory(ctx, directory) {
@@ -89,13 +110,45 @@ async function locateInclude(ctx, scope, include, base, environment) {
89
110
  if (!(0, resolve_git_source_1.isWithin)(remote.root, path)) {
90
111
  throw new Error(`${include.path} in ${include.git} points outside the repository`);
91
112
  }
92
- return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: include.with };
113
+ return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote, inputs: passEnvs(include.with, scope) };
93
114
  }
94
- const path = (0, path_1.join)(base, include);
115
+ const localPath = typeof include === 'string' ? include : include.path;
116
+ const path = (0, path_1.join)(base, localPath);
95
117
  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}`);
118
+ throw new Error(`${localPath} in ${scope.fileName} points outside ${scope.remote.git}`);
119
+ }
120
+ const inputs = typeof include === 'string' ? undefined : passEnvs(include.with, scope);
121
+ return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote, inputs };
122
+ }
123
+ // `${NAME}` (or `${NAME:-default}`) in a `with` value reads as it does in the including
124
+ // file: the environment or a `.env` file first, then that file's own `envs` value. So one
125
+ // file holds a value such as NODE_VERSION for all it includes, and CI can still set it. A
126
+ // name the file does not declare is left to the included file, which reads it the same way.
127
+ function passEnvs(inputs, scope) {
128
+ if (!inputs) {
129
+ return undefined;
130
+ }
131
+ const envs = scope.schema.envs ?? {};
132
+ const result = {};
133
+ for (const [key, value] of Object.entries(inputs)) {
134
+ result[key] =
135
+ typeof value === 'string'
136
+ ? value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, fallback) => name in envs ? passEnv(name, `${envs[name]}`, fallback) : match)
137
+ : value;
138
+ }
139
+ return result;
140
+ }
141
+ // The including file's value as an interpolation the included file resolves in the same
142
+ // order. A value built from other variables is passed on as it is written.
143
+ function passEnv(name, value, fallback) {
144
+ const reference = /^\$([A-Za-z_][A-Za-z0-9_]*)$/.exec(value);
145
+ if (reference) {
146
+ return fallback === undefined ? `\${${reference[1]}}` : `\${${reference[1]}:-${fallback}}`;
147
+ }
148
+ if (value.includes('$') || value.includes('}')) {
149
+ return value;
97
150
  }
98
- return { fileName: await (0, default_build_file_1.getBuildFilename)(path, environment), remote: scope.remote };
151
+ return `\${${name}:-${value}}`;
99
152
  }
100
153
  // `includes pull` fetches each source once, however often the build files name it.
101
154
  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
@@ -150,9 +150,11 @@ function candidates(command, path) {
150
150
  .map((dir) => (0, path_1.resolve)(dir, command));
151
151
  return bases.flatMap((base) => extensions.map((extension) => base + extension));
152
152
  }
153
+ // bigint: a Windows file id has more digits than a number keeps, and two files whose ids
154
+ // round to the same number would be the same file
153
155
  function statOf(path) {
154
156
  try {
155
- const stat = (0, fs_1.statSync)(path);
157
+ const stat = (0, fs_1.statSync)(path, { bigint: true });
156
158
  return stat.isFile() ? stat : null;
157
159
  }
158
160
  catch {
@@ -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
  }
@@ -11,10 +11,12 @@ function emptySecretCatalog() {
11
11
  exports.emptySecretCatalog = emptySecretCatalog;
12
12
  const FULL_COMMIT = /^[0-9a-f]{40}$/i;
13
13
  // Providers and accounts are global names, like caches, collected from every
14
- // loaded build file. A name declared twice is an error rather than a silent
15
- // override: which definition runs with a service account's credential must not
16
- // depend on include order.
17
- function collectSecretCatalog(files, environment, context) {
14
+ // loaded build file. A name declared twice is an error rather than a silent override:
15
+ // which definition runs with a service account's credential must not depend on include
16
+ // order. The host config then replaces what the build files declare, name by name (and
17
+ // its default account replaces theirs): the machine that runs a task says how it reads
18
+ // secrets, and a build file that still declares them loads as it is.
19
+ function collectSecretCatalog(files, environment, context, host) {
18
20
  const catalog = emptySecretCatalog();
19
21
  for (const file of files) {
20
22
  const providers = Object.entries(file.schema.secretProviders ?? {});
@@ -28,25 +30,13 @@ function collectSecretCatalog(files, environment, context) {
28
30
  if (catalog.providers[name]) {
29
31
  throw new Error(`secret provider ${name} is declared in ${catalog.providers[name].declaredIn} and in ${file.fileName}`);
30
32
  }
31
- catalog.providers[name] = {
32
- name,
33
- type: 'command',
34
- command: provider.command.map((part) => applyEnvs(part, envs)),
35
- env: provider.env ?? {},
36
- timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
37
- declaredIn: file.fileName,
38
- };
33
+ catalog.providers[name] = toProvider(name, provider, envs, file.fileName);
39
34
  }
40
35
  for (const [name, account] of accounts) {
41
36
  if (catalog.accounts[name]) {
42
37
  throw new Error(`secret account ${name} is declared in ${catalog.accounts[name].declaredIn} and in ${file.fileName}`);
43
38
  }
44
- catalog.accounts[name] = {
45
- name,
46
- default: account.default ?? false,
47
- providers: account.providers,
48
- declaredIn: file.fileName,
49
- };
39
+ catalog.accounts[name] = toAccount(name, account, file.fileName);
50
40
  }
51
41
  }
52
42
  const defaults = Object.values(catalog.accounts).filter((account) => account.default);
@@ -55,14 +45,48 @@ function collectSecretCatalog(files, environment, context) {
55
45
  }
56
46
  for (const account of Object.values(catalog.accounts)) {
57
47
  for (const provider of Object.keys(account.providers)) {
58
- if (!catalog.providers[provider]) {
48
+ if (!catalog.providers[provider] && !host?.secretProviders?.[provider]) {
59
49
  throw new Error(`secret account ${account.name} (${account.declaredIn}) names provider ${provider}, which is not declared`);
60
50
  }
61
51
  }
62
52
  }
53
+ if (host) {
54
+ // the host config has no envs of its own: `${NAME}` is read from the process
55
+ for (const [name, provider] of Object.entries(host.secretProviders ?? {})) {
56
+ catalog.providers[name] = toProvider(name, provider, {}, host.fileName);
57
+ }
58
+ const accounts = Object.entries(host.secretAccounts ?? {});
59
+ const hostDefaults = accounts.filter(([, account]) => account.default);
60
+ if (hostDefaults.length > 1) {
61
+ throw new Error(`more than one default secret account: ${hostDefaults.map(([name]) => name).join(', ')} (${host.fileName})`);
62
+ }
63
+ if (hostDefaults.length > 0) {
64
+ for (const account of Object.values(catalog.accounts)) {
65
+ account.default = false;
66
+ }
67
+ }
68
+ // a provider an account of the host names is only checked when a secret uses it:
69
+ // a project that doesn't declare it, and reads no secret, loads anyway
70
+ for (const [name, account] of accounts) {
71
+ catalog.accounts[name] = toAccount(name, account, host.fileName);
72
+ }
73
+ }
63
74
  return catalog;
64
75
  }
65
76
  exports.collectSecretCatalog = collectSecretCatalog;
77
+ function toProvider(name, provider, envs, declaredIn) {
78
+ return {
79
+ name,
80
+ type: 'command',
81
+ command: provider.command.map((part) => applyEnvs(part, envs)),
82
+ env: provider.env ?? {},
83
+ timeoutMs: (0, units_1.parseDuration)(provider.timeout ?? DEFAULT_TIMEOUT),
84
+ declaredIn,
85
+ };
86
+ }
87
+ function toAccount(name, account, declaredIn) {
88
+ return { name, default: account.default ?? false, providers: account.providers, declaredIn };
89
+ }
66
90
  // A tag or branch can be moved, and a moved file runs with the credentials of a
67
91
  // service account: providers and accounts from a git include are pinned.
68
92
  function requirePinnedSource(file) {
@@ -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,10 @@ 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,
37
+ observe: grant.observe,
35
38
  });
36
39
  return true;
37
40
  }
@@ -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,36 @@
1
+ "use strict";
2
+ // How CPU and memory read, for the dashboard and the CLI (without the clients of capacity.ts).
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.formatMemory = exports.plural = exports.formatCpu = void 0;
5
+ // cores as a person reads them: 0.25, 1.5, 12
6
+ function formatCpu(cores) {
7
+ if (cores === undefined) {
8
+ return '?';
9
+ }
10
+ if (cores === 0) {
11
+ return '0';
12
+ }
13
+ return cores < 10 ? String(Math.round(cores * 100) / 100) : String(Math.round(cores * 10) / 10);
14
+ }
15
+ exports.formatCpu = formatCpu;
16
+ // a count and what it counts: 1 pod, 2 pods
17
+ function plural(count, one, more = `${one}s`) {
18
+ return `${count} ${count === 1 ? one : more}`;
19
+ }
20
+ exports.plural = plural;
21
+ // bytes as a person reads them: 512 MiB, 3.4 GiB
22
+ function formatMemory(bytes) {
23
+ if (bytes === undefined) {
24
+ return '?';
25
+ }
26
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
27
+ let value = bytes;
28
+ let unit = 0;
29
+ while (value >= 1024 && unit < units.length - 1) {
30
+ value /= 1024;
31
+ unit++;
32
+ }
33
+ return `${value < 10 && unit > 0 ? Math.round(value * 10) / 10 : Math.round(value)} ${units[unit]}`;
34
+ }
35
+ exports.formatMemory = formatMemory;
36
+ //# sourceMappingURL=capacity-format.js.map