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.
- package/build.schema.json +10 -0
- package/dist/cli.js +6 -0
- package/dist/executer/execute-command.js +7 -1
- package/dist/optimizer/get-cache-directory.js +5 -0
- package/dist/planner/utils/append-work-task.js +1 -0
- package/dist/planner/work-secret.js +6 -2
- package/dist/program.js +6 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +158 -0
- package/dist/remote/remote-work-commands.js +224 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +4 -0
- package/dist/schema/build-file-schema.js +3 -0
- package/dist/schema/reference-parser.js +9 -2
- package/dist/schema/runner-schema.js +13 -0
- package/dist/secrets/provider-values.js +4 -0
- package/dist/server/accounts-api.js +42 -0
- package/dist/server/approvals-api.js +104 -0
- package/dist/server/definitions-api.js +163 -0
- package/dist/server/definitions.js +160 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +237 -0
- package/dist/server/git-checkout.js +115 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/plan.js +296 -0
- package/dist/server/policy.js +138 -0
- package/dist/server/proposals-api.js +145 -0
- package/dist/server/proposals.js +173 -0
- package/dist/server/repo-access.js +127 -0
- package/dist/server/repo.js +94 -0
- package/dist/server/run-environment.js +20 -0
- package/dist/server/run-plan.js +44 -0
- package/dist/server/run-request.js +85 -0
- package/dist/server/run-store.js +61 -0
- package/dist/server/run-worker-command.js +17 -0
- package/dist/server/run-worker-main.js +7 -0
- package/dist/server/run-worker.js +179 -0
- package/dist/server/runs.js +432 -0
- package/dist/server/server-command.js +61 -0
- package/dist/server/server-config.js +394 -0
- package/dist/server/server.js +434 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- 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
|