hammerkit 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/build.schema.json +10 -0
  2. package/dist/cli.js +6 -0
  3. package/dist/executer/execute-command.js +7 -1
  4. package/dist/optimizer/get-cache-directory.js +5 -0
  5. package/dist/planner/utils/append-work-task.js +1 -0
  6. package/dist/planner/work-secret.js +6 -2
  7. package/dist/program.js +6 -0
  8. package/dist/remote/bundle.js +59 -0
  9. package/dist/remote/collect-bundle.js +41 -0
  10. package/dist/remote/credentials.js +93 -0
  11. package/dist/remote/device-login.js +177 -0
  12. package/dist/remote/host-config.js +170 -0
  13. package/dist/remote/location.js +46 -0
  14. package/dist/remote/remote-commands.js +158 -0
  15. package/dist/remote/remote-work-commands.js +224 -0
  16. package/dist/remote/server-client.js +88 -0
  17. package/dist/remote/source-manifest.js +79 -0
  18. package/dist/remote/text-diff.js +89 -0
  19. package/dist/schema/build-file-base-task-schema.js +4 -0
  20. package/dist/schema/build-file-schema.js +3 -0
  21. package/dist/schema/reference-parser.js +9 -2
  22. package/dist/schema/runner-schema.js +13 -0
  23. package/dist/secrets/provider-values.js +4 -0
  24. package/dist/server/accounts-api.js +42 -0
  25. package/dist/server/approvals-api.js +104 -0
  26. package/dist/server/definitions-api.js +163 -0
  27. package/dist/server/definitions.js +160 -0
  28. package/dist/server/discovery.js +9 -0
  29. package/dist/server/executor.js +237 -0
  30. package/dist/server/git-checkout.js +115 -0
  31. package/dist/server/http.js +39 -0
  32. package/dist/server/oauth.js +226 -0
  33. package/dist/server/plan.js +296 -0
  34. package/dist/server/policy.js +138 -0
  35. package/dist/server/proposals-api.js +145 -0
  36. package/dist/server/proposals.js +173 -0
  37. package/dist/server/repo-access.js +127 -0
  38. package/dist/server/repo.js +94 -0
  39. package/dist/server/run-environment.js +20 -0
  40. package/dist/server/run-plan.js +44 -0
  41. package/dist/server/run-request.js +85 -0
  42. package/dist/server/run-store.js +61 -0
  43. package/dist/server/run-worker-command.js +17 -0
  44. package/dist/server/run-worker-main.js +7 -0
  45. package/dist/server/run-worker.js +179 -0
  46. package/dist/server/runs.js +432 -0
  47. package/dist/server/server-command.js +61 -0
  48. package/dist/server/server-config.js +394 -0
  49. package/dist/server/server.js +434 -0
  50. package/dist/server/sources-api.js +106 -0
  51. package/dist/server/sources.js +258 -0
  52. package/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRequestSchema = exports.runOptionsSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const units_1 = require("../utils/units");
6
+ const bundle_1 = require("../remote/bundle");
7
+ const server_config_1 = require("./server-config");
8
+ const repo_1 = require("./repo");
9
+ const name = zod_1.z.string().min(1).max(200);
10
+ // What `POST /v1/runs` takes. Who is asking and which accounts they may use is
11
+ // decided from the token, never from this body; the accounts a run uses are those
12
+ // its secrets are read as, which the server derives from the published build file.
13
+ const duration = zod_1.z.string().refine((value) => {
14
+ try {
15
+ (0, units_1.parseDuration)(value);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }, 'expected a duration like 30s, 5m or 1h30m');
22
+ // The settings of `hammerkit run` that make sense for a run on a server. What
23
+ // the server limits itself (workers, resources) and what only a terminal
24
+ // needs (watch, log mode) are left out.
25
+ exports.runOptionsSchema = zod_1.z
26
+ .object({
27
+ // as --env
28
+ env: name.optional(),
29
+ // as --filter and --exclude: labels
30
+ filter: zod_1.z.array(name).max(20).optional(),
31
+ exclude: zod_1.z.array(name).max(20).optional(),
32
+ // as --cache
33
+ cache: zod_1.z.enum(['checksum', 'modify-date', 'none']).optional(),
34
+ // as --cache-read-only
35
+ cacheReadOnly: zod_1.z.boolean().optional(),
36
+ // as --timeout
37
+ timeout: duration.optional(),
38
+ // as --no-skip-deps when false
39
+ skipDeps: zod_1.z.boolean().optional(),
40
+ })
41
+ .strict();
42
+ exports.runRequestSchema = zod_1.z
43
+ .object({
44
+ // the published build file to run, and the version of it: a hash, or the
45
+ // latest when left out. The server answers with the hash it chose.
46
+ definition: zod_1.z
47
+ .object({
48
+ name: server_config_1.definitionName,
49
+ hash: zod_1.z.union([zod_1.z.literal('latest'), zod_1.z.string().regex(bundle_1.HASH_PATTERN)]).default('latest'),
50
+ })
51
+ .strict(),
52
+ // tasks by their name in the build file, as on the command line; labels in
53
+ // the options can select tasks instead
54
+ tasks: zod_1.z.array(name).max(100).default([]),
55
+ source: zod_1.z.discriminatedUnion('type', [
56
+ zod_1.z
57
+ .object({
58
+ type: zod_1.z.literal('ref'),
59
+ // `host/path`, such as github.com/corp/payments; the server reads it from its own
60
+ // sources, a url is never taken from a request
61
+ repo: zod_1.z.string().transform((value, ctx) => {
62
+ const repo = (0, repo_1.normalizeRepo)(value);
63
+ if (!repo) {
64
+ ctx.addIssue({
65
+ code: zod_1.z.ZodIssueCode.custom,
66
+ message: 'expected a repository like github.com/corp/payments',
67
+ });
68
+ return zod_1.z.NEVER;
69
+ }
70
+ return repo;
71
+ }),
72
+ ref: name,
73
+ })
74
+ .strict(),
75
+ // the name of a tree announced with POST /v1/sources
76
+ zod_1.z.object({ type: zod_1.z.literal('upload'), snapshot: zod_1.z.string().regex(bundle_1.HASH_PATTERN) }).strict(),
77
+ ]),
78
+ options: exports.runOptionsSchema.default({}),
79
+ })
80
+ .strict()
81
+ .refine((request) => request.tasks.length > 0 || (request.options.filter ?? []).length > 0, {
82
+ message: 'name at least one task, or filter by labels',
83
+ path: ['tasks'],
84
+ });
85
+ //# sourceMappingURL=run-request.js.map
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileRunStore = void 0;
4
+ const promises_1 = require("fs/promises");
5
+ const path_1 = require("path");
6
+ const runs_1 = require("./runs");
7
+ /**
8
+ * One file per run, written to a temporary file and renamed, so a crash leaves the
9
+ * old record and not half a new one. A record holds what the server already shows
10
+ * the caller: the state, the plan, the logs as they were masked. Only the user of the
11
+ * server can read the files.
12
+ */
13
+ class FileRunStore {
14
+ constructor(directory) {
15
+ this.directory = directory;
16
+ }
17
+ async save(record) {
18
+ await (0, promises_1.mkdir)(this.directory, { recursive: true, mode: 0o700 });
19
+ const file = (0, path_1.join)(this.directory, `${record.run.id}.json`);
20
+ const temporary = `${file}.${process.pid}.tmp`;
21
+ await (0, promises_1.writeFile)(temporary, JSON.stringify(record), { mode: 0o600 });
22
+ await (0, promises_1.rename)(temporary, file);
23
+ }
24
+ async remove(id) {
25
+ await (0, promises_1.rm)((0, path_1.join)(this.directory, `${id}.json`), { force: true });
26
+ }
27
+ async load() {
28
+ const records = [];
29
+ const skipped = [];
30
+ let names;
31
+ try {
32
+ names = await (0, promises_1.readdir)(this.directory);
33
+ }
34
+ catch {
35
+ return { records, skipped };
36
+ }
37
+ for (const name of names) {
38
+ if (!name.endsWith('.json')) {
39
+ // a record that was being written when the server stopped
40
+ if (name.endsWith('.tmp')) {
41
+ await (0, promises_1.rm)((0, path_1.join)(this.directory, name), { force: true });
42
+ }
43
+ continue;
44
+ }
45
+ try {
46
+ const record = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(this.directory, name), 'utf8'));
47
+ if (!runs_1.RUN_ID_PATTERN.test(record.run.id) || `${record.run.id}.json` !== name || !Array.isArray(record.events)) {
48
+ throw new Error('not a run record');
49
+ }
50
+ records.push(record);
51
+ }
52
+ catch {
53
+ skipped.push(name);
54
+ }
55
+ }
56
+ records.sort((a, b) => a.run.createdAt.localeCompare(b.run.createdAt));
57
+ return { records, skipped };
58
+ }
59
+ }
60
+ exports.FileRunStore = FileRunStore;
61
+ //# sourceMappingURL=run-store.js.map
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerRunWorkerCommand = void 0;
4
+ const run_worker_1 = require("./run-worker");
5
+ // `hammerkit server-run`: what the server starts for each run. It reads the run on its
6
+ // standard input, executes it in the current directory and reports on its standard output.
7
+ // Not for people: the input comes from the server and says which accounts it may read.
8
+ function registerRunWorkerCommand(program, environment) {
9
+ program
10
+ .command('server-run', { hidden: true })
11
+ .description('execute one run for a hammerkit server (internal)')
12
+ .action(async () => {
13
+ await (0, run_worker_1.workerMain)(process.stdin, environment.stdout, environment.processEnvs, environment.cwd, environment.abortCtrl);
14
+ });
15
+ }
16
+ exports.registerRunWorkerCommand = registerRunWorkerCommand;
17
+ //# sourceMappingURL=run-worker-command.js.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const run_worker_1 = require("./run-worker");
4
+ // The process of a run without the command line, for tests that run from source
5
+ // (`vite-node src/server/run-worker-main.ts`). A build uses `hammerkit server-run`.
6
+ (0, run_worker_1.workerMain)(process.stdin, process.stdout, process.env, process.cwd(), new AbortController()).then(() => process.exit(0), () => process.exit(1));
7
+ //# sourceMappingURL=run-worker-main.js.map
@@ -0,0 +1,179 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workerMain = exports.runTasks = exports.workerInputSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const cli_1 = require("../cli");
6
+ const get_file_context_1 = require("../file/get-file-context");
7
+ const log_1 = require("../log");
8
+ const schema_parser_1 = require("../schema/schema-parser");
9
+ const reference_parser_1 = require("../schema/reference-parser");
10
+ const work_scope_parser_1 = require("../schema/work-scope-parser");
11
+ const work_item_status_1 = require("../planner/work-item-status");
12
+ const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
13
+ const redact_1 = require("../utils/redact");
14
+ const empty_writable_1 = require("../utils/empty-writable");
15
+ const units_1 = require("../utils/units");
16
+ const run_request_1 = require("./run-request");
17
+ const run_plan_1 = require("./run-plan");
18
+ // one log line of a task is cut here, the registry caps how many events a run keeps
19
+ const MAX_LOG_TEXT = 8 * 1024;
20
+ /**
21
+ * What the process that executes a run is told, on its standard input. A run executes
22
+ * in a process of its own, started with an environment the server built, so what the
23
+ * engine reads of its process (the home directory, the docker host, the cache directory)
24
+ * is the run's and not the server's.
25
+ */
26
+ exports.workerInputSchema = zod_1.z
27
+ .object({
28
+ // the build file to start from, in the checkout
29
+ entry: zod_1.z.string().min(1),
30
+ tasks: zod_1.z.array(zod_1.z.string()),
31
+ options: run_request_1.runOptionsSchema,
32
+ // the accounts the run was admitted for; a secret of any other is refused
33
+ accounts: zod_1.z.array(zod_1.z.string()),
34
+ // the operator's providers and accounts, as the server read them from its config
35
+ catalog: zod_1.z.custom((value) => typeof value === 'object' && value !== null),
36
+ })
37
+ .strict();
38
+ /**
39
+ * Executes the tasks of a run with the engine of `hammerkit run`. The caller says which
40
+ * process this is in: it reports what happens through `emit` and stops when `abortCtrl`
41
+ * is aborted.
42
+ */
43
+ async function runTasks(input, cwd, processEnvs, abortCtrl,
44
+ // whether the run was stopped from outside; the engine aborts too when a task fails
45
+ cancelled, emit) {
46
+ const secrets = (0, redact_1.createSecretRegistry)();
47
+ const status = (0, work_item_status_1.statusConsole)((0, empty_writable_1.emptyWritable)(), secrets);
48
+ // what the engine would print to a terminal is not wanted, its status console
49
+ // is what is streamed: it is redacted with the secrets of this run
50
+ const environment = {
51
+ processEnvs,
52
+ hosted: { catalog: input.catalog, accounts: input.accounts },
53
+ abortCtrl,
54
+ cwd,
55
+ file: (0, get_file_context_1.getFileContext)(cwd),
56
+ console: (0, log_1.consoleContext)((0, empty_writable_1.emptyWritable)()),
57
+ status,
58
+ secrets,
59
+ stdout: (0, empty_writable_1.emptyWritable)(),
60
+ stderr: (0, empty_writable_1.emptyWritable)(),
61
+ stdoutColumns: 120,
62
+ };
63
+ const listener = status.on((message) => {
64
+ if (message.type === 'console') {
65
+ emit({
66
+ type: 'log',
67
+ task: message.context.name,
68
+ stream: message.console,
69
+ text: message.message.slice(0, MAX_LOG_TEXT),
70
+ });
71
+ }
72
+ else if (message.level === 'error' || message.level === 'warn') {
73
+ emit({
74
+ type: 'log',
75
+ task: message.context.name,
76
+ stream: 'stderr',
77
+ text: message.message.slice(0, MAX_LOG_TEXT),
78
+ });
79
+ }
80
+ });
81
+ try {
82
+ const options = input.options;
83
+ const execOptions = {
84
+ logMode: 'live',
85
+ cacheDefault: options.cache ?? 'checksum',
86
+ cacheReadOnly: options.cacheReadOnly ?? false,
87
+ timeout: options.timeout ? (0, units_1.parseDuration)(options.timeout) : null,
88
+ skipDeps: options.skipDeps !== false,
89
+ };
90
+ for (const { name, scope } of (0, run_plan_1.getRunScopes)(input.tasks, options)) {
91
+ const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(input.entry, environment);
92
+ const workTree = (0, work_scope_parser_1.getWorkContext)(await (0, reference_parser_1.parseReferences)(ctx, parsed, environment), scope, environment);
93
+ const cli = (0, cli_1.getCli)(workTree, environment);
94
+ if (cli.tasks().length === 0) {
95
+ throw new Error(`no tasks found for ${name}`);
96
+ }
97
+ const execution = cli.exec({ ...execOptions, workers: cli.defaultWorkers() });
98
+ const seen = new Map();
99
+ const watch = execution.state.on('run-events', () => reportTasks(emit, execution.state.current, seen));
100
+ try {
101
+ const result = await execution.start();
102
+ reportTasks(emit, execution.state.current, seen);
103
+ if (!result.success) {
104
+ throw new Error(cancelled() ? 'the run was cancelled' : `${name} failed`);
105
+ }
106
+ }
107
+ finally {
108
+ watch.close();
109
+ }
110
+ }
111
+ }
112
+ finally {
113
+ listener.close();
114
+ }
115
+ }
116
+ exports.runTasks = runTasks;
117
+ // A task event when a task has moved on since the last look.
118
+ function reportTasks(emit, workTree, seen) {
119
+ for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
120
+ const state = task.state.current;
121
+ let event = null;
122
+ if (state.type === 'running') {
123
+ event = 'running';
124
+ }
125
+ else if (state.type === 'completed') {
126
+ event = state.skipped ? null : state.cached ? 'cached' : 'succeeded';
127
+ }
128
+ else if (state.type === 'error' || state.type === 'crash') {
129
+ event = 'failed';
130
+ }
131
+ if (event && seen.get(task.name) !== event) {
132
+ seen.set(task.name, event);
133
+ emit({ type: 'task', task: task.name, state: event });
134
+ }
135
+ }
136
+ }
137
+ async function readAll(stream) {
138
+ const chunks = [];
139
+ for await (const chunk of stream) {
140
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
141
+ }
142
+ return Buffer.concat(chunks).toString('utf8');
143
+ }
144
+ /**
145
+ * The process of one run: reads its input, executes the tasks in the current directory
146
+ * and reports as lines of JSON. It exits 0 when it could say how the run ended, and the
147
+ * server reads that from the last line; a process that dies before has crashed.
148
+ */
149
+ async function workerMain(stdin, stdout, processEnvs, cwd, abortCtrl) {
150
+ const say = (message) => stdout.write(`${JSON.stringify(message)}\n`);
151
+ // the server stops a run with a signal
152
+ let cancelled = false;
153
+ const stop = () => {
154
+ cancelled = true;
155
+ abortCtrl.abort();
156
+ };
157
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
158
+ signals.forEach((signal) => process.on(signal, stop));
159
+ let input;
160
+ try {
161
+ input = exports.workerInputSchema.parse(JSON.parse(await readAll(stdin)));
162
+ }
163
+ catch (e) {
164
+ say({ kind: 'result', ok: false, error: `the run could not be read: ${e instanceof Error ? e.message : e}` });
165
+ return;
166
+ }
167
+ try {
168
+ await runTasks(input, cwd, processEnvs, abortCtrl, () => cancelled, (event) => say({ kind: 'event', event }));
169
+ say({ kind: 'result', ok: true });
170
+ }
171
+ catch (e) {
172
+ say({ kind: 'result', ok: false, error: e instanceof Error ? e.message : String(e) });
173
+ }
174
+ finally {
175
+ signals.forEach((signal) => process.off(signal, stop));
176
+ }
177
+ }
178
+ exports.workerMain = workerMain;
179
+ //# sourceMappingURL=run-worker.js.map