hammerkit 1.27.0-next.1 → 1.27.0-next.3
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/dist/harness/sandbox-client.js +104 -66
- package/dist/log.js +1 -1
- package/dist/program.js +8 -4
- package/dist/remote/annotations.js +13 -1
- package/dist/remote/remote-commands.js +11 -2
- package/dist/remote/remote-work-commands.js +89 -1
- package/dist/secrets/github-app-token-command.js +4 -1
- package/dist/server/kubernetes-backend.js +10 -2
- package/dist/server/run-request.js +6 -0
- package/dist/server/server.js +6 -0
- package/package.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.oneAtATime = exports.SandboxClient = exports.SandboxClientError = void 0;
|
|
3
|
+
exports.quotePosix = exports.fetchFailure = exports.toSandboxPath = exports.checkSandboxPath = exports.exitCodeOf = exports.followRun = exports.submitRun = exports.FINAL_RUN_STATES = exports.oneAtATime = exports.SandboxClient = exports.SandboxClientError = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const token_source_1 = require("../remote/token-source");
|
|
6
6
|
// a write carries up to 32 MiB
|
|
7
7
|
const TIMEOUT_MS = 60000;
|
|
8
8
|
// a run is asked for this long at a time, the log is read in between
|
|
9
9
|
const POLL_WAIT = '1s';
|
|
10
|
+
// a cancelled run waits this long for the server to take the cancel, before it gives up
|
|
11
|
+
const CANCEL_WAIT_MS = 10000;
|
|
10
12
|
class SandboxClientError extends Error {
|
|
11
13
|
constructor(message, status) {
|
|
12
14
|
super(message);
|
|
@@ -115,72 +117,26 @@ class SandboxClient {
|
|
|
115
117
|
if (options.timeoutSeconds) {
|
|
116
118
|
runOptions.timeout = `${Math.max(1, Math.ceil(options.timeoutSeconds))}s`;
|
|
117
119
|
}
|
|
118
|
-
const submitted = await
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
truncated = page.truncated;
|
|
138
|
-
for (const line of page.lines) {
|
|
139
|
-
lines.push(line.text);
|
|
140
|
-
options.onLine?.(line.text);
|
|
141
|
-
from = line.line + 1;
|
|
142
|
-
}
|
|
143
|
-
if (!page.next) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
120
|
+
const submitted = await submitRun(this.connection, {
|
|
121
|
+
definition: this.build.hash,
|
|
122
|
+
tasks: [task],
|
|
123
|
+
source: { type: 'sandbox', id: this.id },
|
|
124
|
+
options: runOptions,
|
|
125
|
+
});
|
|
126
|
+
const followed = await followRun(this.connection, submitted, {
|
|
127
|
+
task,
|
|
128
|
+
signal: options.signal,
|
|
129
|
+
onLine: options.onLine ? (line) => options.onLine?.(line.text) : undefined,
|
|
130
|
+
onWaiting: options.onWaiting,
|
|
131
|
+
});
|
|
132
|
+
const result = followed.run.results?.[task];
|
|
133
|
+
return {
|
|
134
|
+
run: followed.run,
|
|
135
|
+
result,
|
|
136
|
+
output: followed.lines.map((line) => line.text).join('\n'),
|
|
137
|
+
truncated: followed.truncated,
|
|
138
|
+
exitCode: exitCodeOf(result),
|
|
147
139
|
};
|
|
148
|
-
try {
|
|
149
|
-
let run = submitted;
|
|
150
|
-
let waitingSaid = false;
|
|
151
|
-
for (;;) {
|
|
152
|
-
if (options.signal?.aborted) {
|
|
153
|
-
throw new SandboxClientError('the run was cancelled');
|
|
154
|
-
}
|
|
155
|
-
if (run.state === 'awaiting-approval' && !waitingSaid) {
|
|
156
|
-
waitingSaid = true;
|
|
157
|
-
options.onWaiting?.(run);
|
|
158
|
-
}
|
|
159
|
-
await readLog();
|
|
160
|
-
if (['succeeded', 'failed', 'cancelled', 'rejected'].includes(run.state)) {
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
run = await json(await call(this.connection, 'GET', path, { query: { wait: POLL_WAIT } }));
|
|
164
|
-
}
|
|
165
|
-
const result = run.results?.[task];
|
|
166
|
-
if (truncated && result) {
|
|
167
|
-
// the end of a long log is kept in the result
|
|
168
|
-
for (const line of ['… the server cut the log off at its limit, the last lines were:', ...result.lastLog]) {
|
|
169
|
-
lines.push(line);
|
|
170
|
-
options.onLine?.(line);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
run,
|
|
175
|
-
result,
|
|
176
|
-
output: lines.join('\n'),
|
|
177
|
-
truncated,
|
|
178
|
-
exitCode: exitCodeOf(result),
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
finally {
|
|
182
|
-
options.signal?.removeEventListener('abort', cancel);
|
|
183
|
-
}
|
|
184
140
|
}
|
|
185
141
|
/**
|
|
186
142
|
* A shell command on the sandbox: the shell task of the build file with the command as
|
|
@@ -245,6 +201,88 @@ function oneAtATime() {
|
|
|
245
201
|
};
|
|
246
202
|
}
|
|
247
203
|
exports.oneAtATime = oneAtATime;
|
|
204
|
+
// a run that is over, its state does not change any more
|
|
205
|
+
exports.FINAL_RUN_STATES = ['succeeded', 'failed', 'cancelled', 'rejected'];
|
|
206
|
+
// Submits a run, as `POST /v1/runs` takes it, and answers it as the server queued it or
|
|
207
|
+
// holds it for an approval.
|
|
208
|
+
async function submitRun(connection, request) {
|
|
209
|
+
return json(await call(connection, 'POST', '/v1/runs', { body: request }));
|
|
210
|
+
}
|
|
211
|
+
exports.submitRun = submitRun;
|
|
212
|
+
/**
|
|
213
|
+
* Follows a run to its end: its log as it grows, and its state. A signal cancels the run
|
|
214
|
+
* on the server and fails the follow.
|
|
215
|
+
*/
|
|
216
|
+
async function followRun(connection, submitted, options = {}) {
|
|
217
|
+
const path = `/v1/runs/${encodeURIComponent(submitted.id)}`;
|
|
218
|
+
let cancelling;
|
|
219
|
+
const cancel = () => {
|
|
220
|
+
cancelling ?? (cancelling = call(connection, 'POST', `${path}/cancel`).catch(() => undefined));
|
|
221
|
+
};
|
|
222
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
223
|
+
const lines = [];
|
|
224
|
+
let from = 1;
|
|
225
|
+
let truncated = false;
|
|
226
|
+
const add = (line) => {
|
|
227
|
+
lines.push(line);
|
|
228
|
+
options.onLine?.(line);
|
|
229
|
+
};
|
|
230
|
+
const readLog = async () => {
|
|
231
|
+
for (;;) {
|
|
232
|
+
const page = await json(await call(connection, 'GET', `${path}/logs`, { query: { task: options.task, from, limit: 1000 } }));
|
|
233
|
+
truncated = page.truncated;
|
|
234
|
+
for (const line of page.lines) {
|
|
235
|
+
add(line);
|
|
236
|
+
from = line.line + 1;
|
|
237
|
+
}
|
|
238
|
+
if (!page.next) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
let run = submitted;
|
|
245
|
+
let waitingSaid = false;
|
|
246
|
+
for (;;) {
|
|
247
|
+
if (options.signal?.aborted) {
|
|
248
|
+
// the process may exit after this, so the cancel has to reach the server first
|
|
249
|
+
cancel();
|
|
250
|
+
let timer;
|
|
251
|
+
await Promise.race([cancelling, new Promise((resolve) => (timer = setTimeout(resolve, CANCEL_WAIT_MS)))]);
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
throw new SandboxClientError('the run was cancelled');
|
|
254
|
+
}
|
|
255
|
+
if (run.state === 'awaiting-approval' && !waitingSaid) {
|
|
256
|
+
waitingSaid = true;
|
|
257
|
+
options.onWaiting?.(run);
|
|
258
|
+
}
|
|
259
|
+
await readLog();
|
|
260
|
+
if (exports.FINAL_RUN_STATES.includes(run.state)) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
run = await json(await call(connection, 'GET', path, { query: { wait: POLL_WAIT } }));
|
|
264
|
+
}
|
|
265
|
+
if (truncated) {
|
|
266
|
+
// the end of a long log is kept in the results
|
|
267
|
+
const tasks = options.task !== undefined ? [options.task] : Object.keys(run.results ?? {});
|
|
268
|
+
for (const task of tasks) {
|
|
269
|
+
const result = run.results?.[task];
|
|
270
|
+
if (!result) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const said = options.task !== undefined ? 'the last lines were' : `the last lines of ${task} were`;
|
|
274
|
+
for (const text of [`… the server cut the log off at its limit, ${said}:`, ...result.lastLog]) {
|
|
275
|
+
add({ line: 0, task, stream: 'stdout', text });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { run, lines, truncated };
|
|
280
|
+
}
|
|
281
|
+
finally {
|
|
282
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
exports.followRun = followRun;
|
|
248
286
|
function exitCodeOf(result) {
|
|
249
287
|
if (!result) {
|
|
250
288
|
return null;
|
package/dist/log.js
CHANGED
|
@@ -216,7 +216,7 @@ function writeWorkItemLogToConsole(env, log, maxNodeNameLength) {
|
|
|
216
216
|
exports.writeWorkItemLogToConsole = writeWorkItemLogToConsole;
|
|
217
217
|
function formatDate(date) {
|
|
218
218
|
return colors_1.default.grey(`[${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date
|
|
219
|
-
.
|
|
219
|
+
.getDate()
|
|
220
220
|
.toString()
|
|
221
221
|
.padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date
|
|
222
222
|
.getMinutes()
|
package/dist/program.js
CHANGED
|
@@ -159,11 +159,14 @@ async function getProgram(environment, argv) {
|
|
|
159
159
|
if (fileIndex >= 0) {
|
|
160
160
|
args.splice(fileIndex, 2);
|
|
161
161
|
}
|
|
162
|
-
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName);
|
|
162
|
+
(0, remote_commands_1.registerRemoteCommands)(program, environment, fileName, passthrough);
|
|
163
163
|
program.hook('preAction', (_, actionCommand) => {
|
|
164
|
-
const takesArguments = actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name())
|
|
164
|
+
const takesArguments = (actionCommand.parent === program && ['run', 'up'].includes(actionCommand.name())) ||
|
|
165
|
+
(actionCommand.parent?.name() === 'remote' && actionCommand.name() === 'run');
|
|
165
166
|
if (passthrough.length > 0 && !takesArguments) {
|
|
166
|
-
program.error(`${actionCommand.name()} takes no arguments after --, only run and
|
|
167
|
+
program.error(`${actionCommand.name()} takes no arguments after --, only run, up and remote run do`, {
|
|
168
|
+
exitCode: 1,
|
|
169
|
+
});
|
|
167
170
|
}
|
|
168
171
|
});
|
|
169
172
|
if (await environment.file.exists(fileName)) {
|
|
@@ -780,7 +783,8 @@ tasks:
|
|
|
780
783
|
program.option('--file', 'set build file', '.hammerkit.yaml');
|
|
781
784
|
program.option('--set <NAME=value>', 'value for $NAME / ${NAME} references, ahead of the shell and .env (repeatable)');
|
|
782
785
|
program.configureOutput({
|
|
783
|
-
|
|
786
|
+
// --version and --help as they are, so `VERSION=$(hammerkit --version)` works
|
|
787
|
+
writeOut: (str) => environment.stdout.write(str),
|
|
784
788
|
writeErr: (str) => {
|
|
785
789
|
environment.console.error(str);
|
|
786
790
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.applyAnnotations = exports.collectAnnotations = exports.remoteRepo = void 0;
|
|
3
|
+
exports.applyAnnotations = exports.collectAnnotations = exports.readCheckout = exports.remoteRepo = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const os_1 = require("os");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -36,6 +36,18 @@ function remoteRepo(url) {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
exports.remoteRepo = remoteRepo;
|
|
39
|
+
// The checkout a directory is in: the repository of its `origin`, the branch it is on and
|
|
40
|
+
// its commit, what can be found of them.
|
|
41
|
+
async function readCheckout(cwd) {
|
|
42
|
+
if ((await git(cwd, 'rev-parse', '--is-inside-work-tree')) !== 'true') {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const remote = await git(cwd, 'remote', 'get-url', 'origin');
|
|
46
|
+
// a detached HEAD is on no branch
|
|
47
|
+
const branch = await git(cwd, 'symbolic-ref', '--quiet', '--short', 'HEAD');
|
|
48
|
+
return { repo: remote ? remoteRepo(remote) : undefined, branch, commit: await git(cwd, 'rev-parse', 'HEAD') };
|
|
49
|
+
}
|
|
50
|
+
exports.readCheckout = readCheckout;
|
|
39
51
|
// Where a build file is published from: its path in its repository (or on this
|
|
40
52
|
// machine, outside of one), the repository and commit, this machine and its user.
|
|
41
53
|
// `files` are the paths of the bundle, relative to the build file's directory: when
|
|
@@ -16,7 +16,9 @@ const remote_work_commands_1 = require("./remote-work-commands");
|
|
|
16
16
|
const mcp_command_1 = require("../harness/mcp-command");
|
|
17
17
|
// Host-level commands: they only touch the machine's config file, so they work
|
|
18
18
|
// with or without a build file in the working directory.
|
|
19
|
-
function registerRemoteCommands(program, environment, buildFile
|
|
19
|
+
function registerRemoteCommands(program, environment, buildFile,
|
|
20
|
+
// what follows `--`, the arguments of `remote run`
|
|
21
|
+
passthrough = []) {
|
|
20
22
|
const remote = program.command('remote').description('register the servers runs can be sent to');
|
|
21
23
|
const guarded = (failure, fn) => {
|
|
22
24
|
return async (...args) => {
|
|
@@ -154,7 +156,14 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
154
156
|
environment.stdout.write(`repositories: ${me.repos.map(server_client_1.printable).join(', ')}\n`);
|
|
155
157
|
}
|
|
156
158
|
}));
|
|
157
|
-
(0, remote_work_commands_1.registerWorkCommands)(remote, {
|
|
159
|
+
(0, remote_work_commands_1.registerWorkCommands)(remote, {
|
|
160
|
+
environment,
|
|
161
|
+
buildFile,
|
|
162
|
+
getServer,
|
|
163
|
+
guarded,
|
|
164
|
+
passthrough,
|
|
165
|
+
fail: (message, exitCode) => program.error(message, { exitCode }),
|
|
166
|
+
});
|
|
158
167
|
(0, mcp_command_1.registerMcpCommand)(remote, environment);
|
|
159
168
|
}
|
|
160
169
|
exports.registerRemoteCommands = registerRemoteCommands;
|
|
@@ -12,6 +12,8 @@ const bundle_1 = require("./bundle");
|
|
|
12
12
|
const annotations_1 = require("./annotations");
|
|
13
13
|
const server_client_1 = require("./server-client");
|
|
14
14
|
const bundle_text_1 = require("./bundle-text");
|
|
15
|
+
const sandbox_client_1 = require("../harness/sandbox-client");
|
|
16
|
+
const path_1 = require("path");
|
|
15
17
|
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
16
18
|
// what the publisher said about where a version comes from, one line
|
|
17
19
|
// What a publisher said about where a version comes from: the server takes it as said,
|
|
@@ -29,6 +31,11 @@ function yankLines(version) {
|
|
|
29
31
|
]
|
|
30
32
|
: [];
|
|
31
33
|
}
|
|
34
|
+
// A line a task printed, shown as it is but for control characters, so it can't rewrite
|
|
35
|
+
// the terminal; tabs stay.
|
|
36
|
+
function printableLine(text) {
|
|
37
|
+
return text.split('\t').map(server_client_1.printableWhole).join('\t');
|
|
38
|
+
}
|
|
32
39
|
const collectOption = (value, previous = []) => [...previous, value];
|
|
33
40
|
// What a person who approves is shown of a run: everything the decision rests on.
|
|
34
41
|
function describeApproval(run) {
|
|
@@ -89,7 +96,7 @@ function describePlan(plan) {
|
|
|
89
96
|
}
|
|
90
97
|
exports.describePlan = describePlan;
|
|
91
98
|
function registerWorkCommands(remote, context) {
|
|
92
|
-
const { environment, buildFile, getServer, guarded } = context;
|
|
99
|
+
const { environment, buildFile, getServer, guarded, passthrough, fail } = context;
|
|
93
100
|
const connect = async (name) => {
|
|
94
101
|
const chosen = await getServer(name);
|
|
95
102
|
// a CI job or an agent brings its own token, a person signs in
|
|
@@ -122,6 +129,87 @@ function registerWorkCommands(remote, context) {
|
|
|
122
129
|
environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
|
|
123
130
|
}
|
|
124
131
|
}));
|
|
132
|
+
remote
|
|
133
|
+
.command('run <tasks...>')
|
|
134
|
+
.description('run tasks of a published build file on a server from a git ref, print their output and exit as they did')
|
|
135
|
+
.addOption(new commander_1.Option('--server <name>', 'the server to run on; the default of remote use when left out'))
|
|
136
|
+
.addOption(new commander_1.Option('--build <hash>', 'the published version to run (sha256:...), or HAMMERKIT_BUILD; the hash of the build file here when left out'))
|
|
137
|
+
.addOption(new commander_1.Option('--repo <host/path>', 'the repository, such as github.com/corp/payments; origin of the checkout here when left out'))
|
|
138
|
+
.addOption(new commander_1.Option('--ref <ref>', 'the branch or tag to run; the branch checked out here when left out'))
|
|
139
|
+
.addOption(new commander_1.Option('--commit <sha>', 'the commit the ref has to be at, else the run is refused; the commit checked out here when --ref is left out'))
|
|
140
|
+
.addOption(new commander_1.Option('--env <name>', 'the environment of the build file, as --env of run'))
|
|
141
|
+
.addOption(new commander_1.Option('--cache <method>', 'as --cache of run').choices(['checksum', 'modify-date', 'none']))
|
|
142
|
+
.addOption(new commander_1.Option('--timeout <duration>', 'stop the run after this long, such as 30m'))
|
|
143
|
+
.action(guarded('Remote run was not successful', async (tasks, options) => {
|
|
144
|
+
const named = options.build ?? environment.processEnvs.HAMMERKIT_BUILD;
|
|
145
|
+
const definition = named ?? (0, bundle_1.hashBundle)(await (0, collect_bundle_1.collectBundle)(buildFile, environment));
|
|
146
|
+
if (!bundle_1.HASH_PATTERN.test(definition)) {
|
|
147
|
+
throw new Error(`${(0, server_client_1.printable)(definition)} is not the hash of a published version (sha256:...)`);
|
|
148
|
+
}
|
|
149
|
+
const checkout = options.repo && options.ref ? {} : await (0, annotations_1.readCheckout)((0, path_1.dirname)(buildFile));
|
|
150
|
+
const repo = options.repo ?? checkout.repo;
|
|
151
|
+
const ref = options.ref ?? checkout.branch;
|
|
152
|
+
// the commit checked out here is the code meant, when the ref is the branch it is on
|
|
153
|
+
const commit = options.commit ?? (options.ref ? undefined : checkout.commit);
|
|
154
|
+
if (!repo) {
|
|
155
|
+
throw new Error('name the repository with --repo, the checkout here has no origin it can be told by');
|
|
156
|
+
}
|
|
157
|
+
if (!ref) {
|
|
158
|
+
throw new Error('name the branch or tag with --ref, the checkout here is on no branch');
|
|
159
|
+
}
|
|
160
|
+
const { name: serverName, server } = await getServer(options.server);
|
|
161
|
+
const connection = { url: server.url, ...(0, token_source_1.getTokenSource)(environment, serverName, server) };
|
|
162
|
+
const runOptions = {};
|
|
163
|
+
for (const key of ['env', 'cache', 'timeout']) {
|
|
164
|
+
if (options[key] !== undefined) {
|
|
165
|
+
runOptions[key] = options[key];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (passthrough.length > 0) {
|
|
169
|
+
runOptions.args = passthrough;
|
|
170
|
+
}
|
|
171
|
+
let submitted;
|
|
172
|
+
try {
|
|
173
|
+
submitted = await (0, sandbox_client_1.submitRun)(connection, {
|
|
174
|
+
definition,
|
|
175
|
+
tasks,
|
|
176
|
+
source: { type: 'ref', repo, ref, ...(commit ? { commit } : {}) },
|
|
177
|
+
options: runOptions,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e.status === 400 && !named && /no published build file/.test(String(e))) {
|
|
182
|
+
throw new Error(`the build file here is not published on ${serverName} (${definition}), publish it first`);
|
|
183
|
+
}
|
|
184
|
+
throw e;
|
|
185
|
+
}
|
|
186
|
+
const at = submitted.source.type === 'ref' ? ` at ${(0, server_client_1.printable)(submitted.source.commit)}` : '';
|
|
187
|
+
environment.stdout.write(`${(0, server_client_1.printable)(submitted.id)} on ${serverName}: ${tasks.map(server_client_1.printable).join(', ')} from ${(0, server_client_1.printable)(ref)}${at}\n`);
|
|
188
|
+
const prefixed = tasks.length > 1;
|
|
189
|
+
const followed = await (0, sandbox_client_1.followRun)(connection, submitted, {
|
|
190
|
+
signal: environment.abortCtrl.signal,
|
|
191
|
+
onLine: (line) => {
|
|
192
|
+
environment.stdout.write(`${prefixed || line.task !== tasks[0] ? `[${(0, server_client_1.printable)(line.task)}] ` : ''}${printableLine(line.text)}\n`);
|
|
193
|
+
},
|
|
194
|
+
onWaiting: (run) => {
|
|
195
|
+
for (const approval of run.approvals ?? []) {
|
|
196
|
+
environment.stdout.write(`waiting until somebody who uses ${(0, server_client_1.printable)(approval.account)} approves it, before ${(0, server_client_1.printable)(approval.expiresAt)}: hammerkit remote approve ${(0, server_client_1.printable)(run.id)}\n`);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
const run = followed.run;
|
|
201
|
+
if (run.state === 'succeeded') {
|
|
202
|
+
environment.stdout.write(`${(0, server_client_1.printable)(run.id)} succeeded\n`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// the exit code of the first task that failed, as `hammerkit run` would end
|
|
206
|
+
const failed = tasks
|
|
207
|
+
.map((task) => run.results?.[task])
|
|
208
|
+
.concat(Object.values(run.results ?? {}))
|
|
209
|
+
.find((result) => result?.status === 'failed');
|
|
210
|
+
const exitCode = failed?.exitCode && failed.exitCode > 0 ? failed.exitCode : 1;
|
|
211
|
+
fail(`${(0, server_client_1.printable)(run.id)} ${run.state}${run.error ? `: ${(0, server_client_1.printable)(run.error)}` : ''}`, exitCode);
|
|
212
|
+
}));
|
|
125
213
|
remote
|
|
126
214
|
.command('versions [server]')
|
|
127
215
|
.description('list the published versions of build files on a server, newest first, with where they come from')
|
|
@@ -6,6 +6,8 @@ const github_app_token_1 = require("./github-app-token");
|
|
|
6
6
|
// `hammerkit github-app-token <owner/repo>`: a secret provider command for the token a
|
|
7
7
|
// server reads and pushes a repository with, so the released binary mints it without a
|
|
8
8
|
// script or a runtime of its own. It prints the token and nothing else, without a newline.
|
|
9
|
+
// A failure goes to stderr, which the server puts in its error: the CLI's console writes to
|
|
10
|
+
// stdout, and the server drops what a failed provider printed there.
|
|
9
11
|
function registerGithubAppTokenCommand(program, environment) {
|
|
10
12
|
program
|
|
11
13
|
.command('github-app-token <repo>')
|
|
@@ -15,7 +17,8 @@ function registerGithubAppTokenCommand(program, environment) {
|
|
|
15
17
|
environment.stdout.write(await (0, github_app_token_1.githubAppToken)(repo, environment.processEnvs));
|
|
16
18
|
}
|
|
17
19
|
catch (e) {
|
|
18
|
-
|
|
20
|
+
environment.stderr.write(`github-app-token: ${(0, log_1.getErrorMessage)(e)}\n`);
|
|
21
|
+
throw e;
|
|
19
22
|
}
|
|
20
23
|
});
|
|
21
24
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.getNamespacePolicies = exports.getClusterAccess = exports.getCallerNamespace = void 0;
|
|
3
|
+
exports.awaitRunObjectsGone = exports.removeRunObjects = exports.CallerNamespaces = exports.getVolumeScope = exports.KubernetesExecutor = exports.getRunRoleBinding = exports.getNamespacePolicies = exports.getClusterAccess = exports.isCallerNamespace = exports.getCallerNamespace = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
6
|
const os_1 = require("os");
|
|
@@ -32,6 +32,12 @@ function getCallerNamespace(prefix, owner) {
|
|
|
32
32
|
return `${prefix}${(0, crypto_1.createHash)('sha256').update(`${owner.iss}\0${owner.sub}`).digest('hex').slice(0, 16)}`;
|
|
33
33
|
}
|
|
34
34
|
exports.getCallerNamespace = getCallerNamespace;
|
|
35
|
+
// Whether a namespace is one getCallerNamespace makes with this prefix: a server named `hk`
|
|
36
|
+
// passes over the namespaces of a server named `hk-test` on the same cluster.
|
|
37
|
+
function isCallerNamespace(prefix, name) {
|
|
38
|
+
return name.startsWith(prefix) && /^[0-9a-f]{16}$/.test(name.slice(prefix.length));
|
|
39
|
+
}
|
|
40
|
+
exports.isCallerNamespace = isCallerNamespace;
|
|
35
41
|
// How the server and the processes of its runs reach the cluster: a kubeconfig and
|
|
36
42
|
// context the operator names, else the server's default kubeconfig, else the cluster
|
|
37
43
|
// the server runs in (its pod's ServiceAccount, found through the variables
|
|
@@ -335,7 +341,9 @@ class KubernetesExecutor {
|
|
|
335
341
|
.then((namespaces) => {
|
|
336
342
|
for (const namespace of namespaces.items) {
|
|
337
343
|
const name = namespace.metadata?.name;
|
|
338
|
-
if (name
|
|
344
|
+
if (name &&
|
|
345
|
+
isCallerNamespace(this.config.backend.namespacePrefix, name) &&
|
|
346
|
+
!namespace.metadata?.deletionTimestamp) {
|
|
339
347
|
this.sweepLater(name, 0);
|
|
340
348
|
}
|
|
341
349
|
}
|
|
@@ -75,6 +75,12 @@ exports.runRequestSchema = zod_1.z
|
|
|
75
75
|
return repo;
|
|
76
76
|
}),
|
|
77
77
|
ref: name,
|
|
78
|
+
// the commit the ref has to be at, such as the one a CI job checked out: a ref that
|
|
79
|
+
// moved since is refused, so the run executes the code that was asked for
|
|
80
|
+
commit: zod_1.z
|
|
81
|
+
.string()
|
|
82
|
+
.regex(/^([0-9a-f]{40}|[0-9a-f]{64})$/, 'expected a full commit hash, 40 or 64 hex digits')
|
|
83
|
+
.optional(),
|
|
78
84
|
})
|
|
79
85
|
.strict(),
|
|
80
86
|
// the name of a tree announced with POST /v1/sources
|
package/dist/server/server.js
CHANGED
|
@@ -304,6 +304,12 @@ async function submitRun(config, registry, definitions, sources, sandboxes, plan
|
|
|
304
304
|
if (request.source.type === 'ref') {
|
|
305
305
|
try {
|
|
306
306
|
const commit = await repos.resolve(request.source.repo, request.source.ref);
|
|
307
|
+
if (request.source.commit && commit !== request.source.commit) {
|
|
308
|
+
const error = `${request.source.ref} is at ${commit} now, not at ${request.source.commit}`;
|
|
309
|
+
log(`refuse run iss=${principal.iss} sub=${principal.sub}: ${error}`);
|
|
310
|
+
(0, http_2.send)(res, 409, { error });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
307
313
|
source = { type: 'ref', repo: request.source.repo, ref: request.source.ref, commit };
|
|
308
314
|
}
|
|
309
315
|
catch (e) {
|
package/package.json
CHANGED