hammerkit 1.26.0 → 1.27.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.
- package/build.schema.json +53 -1
- package/dist/cache/backends/local-cache-backend.js +10 -2
- package/dist/cache/explain.js +13 -11
- package/dist/cli.js +3 -0
- package/dist/docker/item-network.js +471 -0
- package/dist/docker/pull.js +4 -2
- package/dist/docker/remove-orphaned-containers.js +13 -6
- package/dist/environment/replace-env-variables.js +8 -6
- package/dist/executer/await-completed-dependencies.js +2 -1
- package/dist/executer/check-readiness.js +2 -2
- package/dist/executer/docker-service.js +24 -13
- package/dist/executer/docker-task.js +18 -29
- package/dist/executer/event-cache.js +4 -0
- package/dist/executer/execute-command.js +18 -5
- package/dist/executer/execute-docker.js +4 -1
- package/dist/executer/execution-steps.js +33 -7
- package/dist/executer/get-docker-executor.js +14 -7
- package/dist/executer/scheduler/enqueue-next.js +17 -3
- package/dist/executer/watch-loop.js +2 -2
- package/dist/executer/watch-state-key.js +1 -1
- package/dist/file/within-root.js +42 -0
- package/dist/harness/connect.js +99 -0
- package/dist/harness/format.js +73 -0
- package/dist/harness/mcp-command.js +59 -0
- package/dist/harness/mcp.js +432 -0
- package/dist/harness/opencode.js +492 -0
- package/dist/harness/pi.js +217 -0
- package/dist/harness/sandbox-client.js +373 -0
- package/dist/index.js +33 -36
- package/dist/kubernetes/apply.js +31 -1
- package/dist/kubernetes/await-running-state.js +33 -1
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +43 -21
- package/dist/kubernetes/ensure-kubernetes-persistent-volume-claim-exists.js +131 -4
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +25 -4
- package/dist/kubernetes/ensure-persistent-data.js +48 -7
- package/dist/kubernetes/get-service-ip.js +15 -1
- package/dist/kubernetes/job-placement.js +99 -0
- package/dist/kubernetes/job-secrets.js +306 -0
- package/dist/kubernetes/kept-claims.js +191 -0
- package/dist/kubernetes/kubernetes-instance.js +10 -2
- package/dist/kubernetes/network-gate.js +71 -0
- package/dist/kubernetes/network-policy.js +526 -0
- package/dist/kubernetes/resources.js +13 -2
- package/dist/kubernetes/restore-kubernetes-data.js +19 -4
- package/dist/kubernetes/secrets.js +2 -0
- package/dist/kubernetes/store-kubernetes-data.js +1 -1
- package/dist/kubernetes/volumes.js +100 -11
- package/dist/main.js +41 -0
- package/dist/network/egress-proxy.js +368 -0
- package/dist/network/port-relay.js +35 -0
- package/dist/optimizer/get-work-cache-stats.js +16 -0
- package/dist/optimizer/work-task-cache-description.js +3 -2
- package/dist/planner/service-definition.js +5 -0
- package/dist/planner/utils/append-work-environment.js +2 -0
- package/dist/planner/utils/append-work-service.js +4 -0
- package/dist/planner/utils/append-work-task.js +17 -1
- package/dist/planner/utils/assert-item-paths.js +50 -0
- package/dist/planner/utils/hosted-volumes.js +35 -0
- package/dist/planner/work-network.js +193 -0
- package/dist/planner/work-runtime-docker.js +41 -0
- package/dist/planner/work-runtime-kubernetes.js +335 -102
- package/dist/program.js +21 -0
- package/dist/remote/annotations.js +103 -0
- package/dist/remote/bundle-text.js +24 -0
- package/dist/remote/bundle.js +9 -1
- package/dist/remote/remote-commands.js +4 -1
- package/dist/remote/remote-work-commands.js +182 -69
- package/dist/remote/server-client.js +18 -5
- package/dist/remote/source-manifest.js +15 -2
- package/dist/remote/token-source.js +67 -0
- package/dist/schema/build-file-base-task-schema.js +3 -0
- package/dist/schema/build-file-container-service-schema.js +6 -0
- package/dist/schema/build-file-container-task-schema.js +3 -0
- package/dist/schema/build-file-environment-schema.js +9 -0
- package/dist/schema/network-schema.js +78 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/schema-parser.js +16 -3
- package/dist/schema/work-scope-parser.js +5 -0
- package/dist/secrets/command-secret-provider.js +73 -10
- package/dist/secrets/github-app-token-command.js +23 -0
- package/dist/secrets/github-app-token.js +191 -0
- package/dist/server/accounts-api.js +9 -2
- package/dist/server/definitions-api.js +69 -47
- package/dist/server/definitions.js +76 -51
- package/dist/server/executor.js +156 -41
- package/dist/server/git-checkout.js +72 -8
- package/dist/server/kubernetes-backend.js +554 -0
- package/dist/server/kubernetes-sandbox-storage.js +996 -0
- package/dist/server/oauth.js +1 -0
- package/dist/server/plan.js +51 -1
- package/dist/server/policy.js +104 -15
- package/dist/server/proposals-api.js +24 -40
- package/dist/server/proposals.js +12 -25
- package/dist/server/repo-access.js +37 -6
- package/dist/server/run-logs.js +217 -0
- package/dist/server/run-plan.js +51 -2
- package/dist/server/run-request.js +23 -10
- package/dist/server/run-store.js +5 -0
- package/dist/server/run-worker-command.js +13 -1
- package/dist/server/run-worker.js +121 -24
- package/dist/server/runs.js +184 -10
- package/dist/server/sandbox-docker.js +62 -0
- package/dist/server/sandbox-files-api.js +269 -0
- package/dist/server/sandbox-files.js +1054 -0
- package/dist/server/sandbox-patch.js +481 -0
- package/dist/server/sandbox-pod-command.js +15 -0
- package/dist/server/sandbox-pod.js +146 -0
- package/dist/server/sandbox-storage.js +462 -0
- package/dist/server/sandboxes-api.js +210 -0
- package/dist/server/sandboxes.js +519 -0
- package/dist/server/server-config.js +412 -22
- package/dist/server/server.js +204 -39
- package/dist/utils/run-id.js +12 -0
- package/harness/opencode.ts +11 -0
- package/harness/pi.ts +7 -0
- package/package.json +8 -2
- package/dist/remote/text-diff.js +0 -89
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyAnnotations = exports.collectAnnotations = exports.remoteRepo = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const util_1 = require("util");
|
|
8
|
+
const repo_1 = require("../server/repo");
|
|
9
|
+
const bundle_1 = require("./bundle");
|
|
10
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
11
|
+
async function git(cwd, ...args) {
|
|
12
|
+
try {
|
|
13
|
+
const value = (await execFileAsync('git', args, { cwd, timeout: 5000 })).stdout.trim();
|
|
14
|
+
return value || undefined;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// `host/path` of a remote, or nothing: a url that is neither https, ssh nor
|
|
21
|
+
// `user@host:path` is left out, so a credential in it never leaves the machine.
|
|
22
|
+
function remoteRepo(url) {
|
|
23
|
+
const scp = /^[\w.-]+@([\w.-]+):(?!\/)(.+)$/.exec(url);
|
|
24
|
+
if (scp) {
|
|
25
|
+
return (0, repo_1.normalizeRepo)(`${scp[1]}/${scp[2]}`) ?? undefined;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
if (!['https:', 'ssh:'].includes(parsed.protocol)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return (0, repo_1.normalizeRepo)(`${parsed.hostname}${parsed.pathname}`) ?? undefined;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.remoteRepo = remoteRepo;
|
|
39
|
+
// Where a build file is published from: its path in its repository (or on this
|
|
40
|
+
// machine, outside of one), the repository and commit, this machine and its user.
|
|
41
|
+
// `files` are the paths of the bundle, relative to the build file's directory: when
|
|
42
|
+
// one of them differs from the commit, `dirty=true` says the version is not that
|
|
43
|
+
// commit's. What can not be found is left out.
|
|
44
|
+
async function collectAnnotations(fileName, files = [(0, path_1.basename)(fileName)]) {
|
|
45
|
+
const path = (0, path_1.resolve)(fileName);
|
|
46
|
+
const cwd = (0, path_1.dirname)(path);
|
|
47
|
+
const annotations = {};
|
|
48
|
+
// git says where the directory is in the repository: comparing paths would trip on
|
|
49
|
+
// what the file system calls the same directory differently (symlinks, 8.3 names)
|
|
50
|
+
const inside = (await git(cwd, 'rev-parse', '--is-inside-work-tree')) === 'true';
|
|
51
|
+
const prefix = inside ? (await git(cwd, 'rev-parse', '--show-prefix')) ?? '' : undefined;
|
|
52
|
+
annotations.path = prefix !== undefined ? `${prefix}${(0, path_1.basename)(path)}` : path;
|
|
53
|
+
if (inside) {
|
|
54
|
+
const remote = await git(cwd, 'remote', 'get-url', 'origin');
|
|
55
|
+
const repo = remote ? remoteRepo(remote) : undefined;
|
|
56
|
+
const commit = await git(cwd, 'rev-parse', 'HEAD');
|
|
57
|
+
if (repo) {
|
|
58
|
+
annotations.repo = repo;
|
|
59
|
+
}
|
|
60
|
+
if (commit) {
|
|
61
|
+
annotations.commit = commit;
|
|
62
|
+
const changed = await git(cwd, '--literal-pathspecs', 'status', '--porcelain', '--untracked-files=all', '--', ...files);
|
|
63
|
+
if (changed) {
|
|
64
|
+
annotations.dirty = 'true';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
annotations.machine = (0, os_1.hostname)();
|
|
69
|
+
try {
|
|
70
|
+
annotations.user = (0, os_1.userInfo)().username;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// a user without an entry in the password database
|
|
74
|
+
}
|
|
75
|
+
return Object.fromEntries(Object.entries(annotations).filter(([, value]) => value));
|
|
76
|
+
}
|
|
77
|
+
exports.collectAnnotations = collectAnnotations;
|
|
78
|
+
// `--annotation key=value` sets one, `key=` leaves it out.
|
|
79
|
+
function applyAnnotations(annotations, options) {
|
|
80
|
+
const result = { ...annotations };
|
|
81
|
+
for (const option of options) {
|
|
82
|
+
const index = option.indexOf('=');
|
|
83
|
+
if (index <= 0) {
|
|
84
|
+
throw new Error(`${option} is not an annotation, write it as key=value`);
|
|
85
|
+
}
|
|
86
|
+
const key = option.slice(0, index);
|
|
87
|
+
const value = option.slice(index + 1);
|
|
88
|
+
if (value) {
|
|
89
|
+
result[key] = value;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
delete result[key];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const parsed = bundle_1.annotationsSchema.safeParse(result);
|
|
96
|
+
if (!parsed.success) {
|
|
97
|
+
const issue = parsed.error.issues[0];
|
|
98
|
+
throw new Error(`${issue.path.length > 0 ? `annotation ${issue.path.join('.')}` : 'annotations'}: ${issue.message}`);
|
|
99
|
+
}
|
|
100
|
+
return parsed.data;
|
|
101
|
+
}
|
|
102
|
+
exports.applyAnnotations = applyAnnotations;
|
|
103
|
+
//# sourceMappingURL=annotations.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.showBundle = exports.visible = void 0;
|
|
4
|
+
// Text from a proposal is shown to somebody who decides on it, so nothing in it may
|
|
5
|
+
// rewrite the terminal or hide what it says: control and format characters (which
|
|
6
|
+
// include the ones that reorder text) are shown as `?`. Long lines are not cut,
|
|
7
|
+
// what is hidden behind a cut is what a reviewer needs to see.
|
|
8
|
+
function visible(line) {
|
|
9
|
+
return line.replace(/\t/g, ' ').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, '?');
|
|
10
|
+
}
|
|
11
|
+
exports.visible = visible;
|
|
12
|
+
const lines = (text) => (text === '' ? [] : text.replace(/\n$/, '').split('\n'));
|
|
13
|
+
// Every file of a version, whole: a version has no other one it could be read
|
|
14
|
+
// against, whoever decides on it reads all of it.
|
|
15
|
+
function showBundle(bundle) {
|
|
16
|
+
return Object.keys(bundle.files)
|
|
17
|
+
.sort()
|
|
18
|
+
.flatMap((path) => [
|
|
19
|
+
`--- ${visible(path)}${path === bundle.entry ? ' (entry)' : ''}`,
|
|
20
|
+
...lines(bundle.files[path]).map((line) => ` ${visible(line)}`),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
exports.showBundle = showBundle;
|
|
24
|
+
//# sourceMappingURL=bundle-text.js.map
|
package/dist/remote/bundle.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HASH_PATTERN = exports.hashBundle = exports.bundleSchema = exports.isBundlePath = exports.MAX_BUNDLE_BYTES = exports.MAX_BUNDLE_FILES = exports.DEFAULT_ENTRY = void 0;
|
|
3
|
+
exports.HASH_PATTERN = exports.hashBundle = exports.bundleSchema = exports.annotationsSchema = exports.MAX_ANNOTATIONS = exports.isBundlePath = exports.MAX_BUNDLE_BYTES = exports.MAX_BUNDLE_FILES = exports.DEFAULT_ENTRY = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
exports.DEFAULT_ENTRY = '.hammerkit.yaml';
|
|
@@ -16,6 +16,14 @@ function isBundlePath(path) {
|
|
|
16
16
|
!path.split('/').some((part) => part === '' || part === '.' || part === '..'));
|
|
17
17
|
}
|
|
18
18
|
exports.isBundlePath = isBundlePath;
|
|
19
|
+
exports.MAX_ANNOTATIONS = 20;
|
|
20
|
+
exports.annotationsSchema = zod_1.z
|
|
21
|
+
.record(zod_1.z
|
|
22
|
+
.string()
|
|
23
|
+
.regex(/^[a-z][a-z0-9._-]{0,63}$/, 'use lower case letters, digits, ".", "_" and "-", starting with a letter'), zod_1.z.string().min(1).max(500))
|
|
24
|
+
.refine((annotations) => Object.keys(annotations).length <= exports.MAX_ANNOTATIONS, {
|
|
25
|
+
message: `at most ${exports.MAX_ANNOTATIONS} annotations`,
|
|
26
|
+
});
|
|
19
27
|
exports.bundleSchema = zod_1.z
|
|
20
28
|
.object({
|
|
21
29
|
entry: zod_1.z.string().default(exports.DEFAULT_ENTRY),
|
|
@@ -10,8 +10,10 @@ const log_1 = require("../log");
|
|
|
10
10
|
const host_config_1 = require("./host-config");
|
|
11
11
|
const device_login_1 = require("./device-login");
|
|
12
12
|
const credentials_1 = require("./credentials");
|
|
13
|
+
const token_source_1 = require("./token-source");
|
|
13
14
|
const server_client_1 = require("./server-client");
|
|
14
15
|
const remote_work_commands_1 = require("./remote-work-commands");
|
|
16
|
+
const mcp_command_1 = require("../harness/mcp-command");
|
|
15
17
|
// Host-level commands: they only touch the machine's config file, so they work
|
|
16
18
|
// with or without a build file in the working directory.
|
|
17
19
|
function registerRemoteCommands(program, environment, buildFile) {
|
|
@@ -129,7 +131,7 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
129
131
|
.description('show who you are on a server and which service accounts you may use')
|
|
130
132
|
.action(guarded('Remote accounts was not successful', async (name) => {
|
|
131
133
|
const chosen = await getServer(name);
|
|
132
|
-
const token = await (0,
|
|
134
|
+
const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
|
|
133
135
|
const me = await (0, server_client_1.getMe)(chosen.server, token);
|
|
134
136
|
const { body } = await (0, server_client_1.callServer)(chosen.server, token, 'GET', '/v1/accounts');
|
|
135
137
|
environment.stdout.write(`${chosen.name}: ${(0, server_client_1.printable)(me.sub)} (${(0, server_client_1.printable)(me.iss)})\n`);
|
|
@@ -153,6 +155,7 @@ function registerRemoteCommands(program, environment, buildFile) {
|
|
|
153
155
|
}
|
|
154
156
|
}));
|
|
155
157
|
(0, remote_work_commands_1.registerWorkCommands)(remote, { environment, buildFile, getServer, guarded });
|
|
158
|
+
(0, mcp_command_1.registerMcpCommand)(remote, environment);
|
|
156
159
|
}
|
|
157
160
|
exports.registerRemoteCommands = registerRemoteCommands;
|
|
158
161
|
//# sourceMappingURL=remote-commands.js.map
|
|
@@ -3,32 +3,54 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.registerWorkCommands = void 0;
|
|
6
|
+
exports.registerWorkCommands = exports.describePlan = exports.describeApproval = void 0;
|
|
7
7
|
const commander_1 = require("commander");
|
|
8
8
|
const colors_1 = __importDefault(require("colors"));
|
|
9
|
-
const
|
|
9
|
+
const token_source_1 = require("./token-source");
|
|
10
10
|
const collect_bundle_1 = require("./collect-bundle");
|
|
11
11
|
const bundle_1 = require("./bundle");
|
|
12
|
+
const annotations_1 = require("./annotations");
|
|
12
13
|
const server_client_1 = require("./server-client");
|
|
13
|
-
const
|
|
14
|
-
const PROPOSAL_NAME = /^([a-z0-9][a-z0-9._-]{0,63})@(sha256:[0-9a-f]{64})$/i;
|
|
15
|
-
// A proposed version is named like `payments@sha256:...`
|
|
16
|
-
function parseProposal(value) {
|
|
17
|
-
const match = PROPOSAL_NAME.exec(value);
|
|
18
|
-
return match && bundle_1.HASH_PATTERN.test(match[2]) ? { name: match[1], hash: match[2] } : undefined;
|
|
19
|
-
}
|
|
14
|
+
const bundle_text_1 = require("./bundle-text");
|
|
20
15
|
const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
|
|
16
|
+
// what the publisher said about where a version comes from, one line
|
|
17
|
+
// What a publisher said about where a version comes from: the server takes it as said,
|
|
18
|
+
// so anybody else is told it is not checked.
|
|
19
|
+
function annotationLines(annotations, label = 'from (unverified)') {
|
|
20
|
+
const entries = Object.entries(annotations ?? {});
|
|
21
|
+
return entries.length > 0
|
|
22
|
+
? [` ${label}: ${entries.map(([key, value]) => `${(0, server_client_1.printable)(key)}=${(0, server_client_1.printable)(value)}`).join(' ')}`]
|
|
23
|
+
: [];
|
|
24
|
+
}
|
|
25
|
+
function yankLines(version) {
|
|
26
|
+
return version.yanked
|
|
27
|
+
? [
|
|
28
|
+
` yanked by ${(0, server_client_1.printable)(version.yanked.by.sub)}${version.yanked.reason ? `: ${(0, server_client_1.printable)(version.yanked.reason)}` : ''}`,
|
|
29
|
+
]
|
|
30
|
+
: [];
|
|
31
|
+
}
|
|
32
|
+
const collectOption = (value, previous = []) => [...previous, value];
|
|
21
33
|
// What a person who approves is shown of a run: everything the decision rests on.
|
|
22
|
-
function
|
|
23
|
-
const source = run.source.type === 'ref'
|
|
24
|
-
|
|
34
|
+
function describeApproval(run) {
|
|
35
|
+
const source = run.source.type === 'ref'
|
|
36
|
+
? `ref ${run.source.ref}`
|
|
37
|
+
: run.source.type === 'sandbox'
|
|
38
|
+
? `sandbox of ${run.source.repo} at ${run.source.commit}, ${run.source.clean ? 'unchanged' : `with changes, files ${run.source.tree}`} (files .gitignore leaves out are not shown)`
|
|
39
|
+
: `snapshot ${run.source.snapshot}`;
|
|
40
|
+
// the arguments are shown whole on a line of their own, each as a JSON string,
|
|
41
|
+
// so commas, spaces and quotes inside one stay visible
|
|
42
|
+
const { args, ...settings } = run.options;
|
|
43
|
+
const options = Object.entries(settings)
|
|
25
44
|
.map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
|
|
26
45
|
.join(' ');
|
|
27
46
|
return [
|
|
28
47
|
`${colors_1.default.bold((0, server_client_1.printable)(run.id))} requested by ${(0, server_client_1.printable)(run.requestedBy.sub)} (${(0, server_client_1.printable)(run.requestedBy.iss)})`,
|
|
29
|
-
`
|
|
48
|
+
` build file ${(0, server_client_1.printable)(run.definition)}`,
|
|
30
49
|
` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
|
|
31
50
|
` accounts: ${run.accounts.length > 0 ? run.accounts.map(server_client_1.printable).join(', ') : 'none'}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
|
|
51
|
+
...(Array.isArray(args) && args.length > 0
|
|
52
|
+
? [` arguments: ${args.map((arg) => (0, server_client_1.printableWhole)(JSON.stringify(arg))).join(' ')}`]
|
|
53
|
+
: []),
|
|
32
54
|
...(run.approvals ?? []).map((approval) => ` ${(0, server_client_1.printable)(approval.account)}: ${approval.approvedBy
|
|
33
55
|
? `approved by ${(0, server_client_1.printable)(approval.approvedBy.sub)}`
|
|
34
56
|
: 'waits for somebody who uses it to approve'}`),
|
|
@@ -36,6 +58,7 @@ function describe(run) {
|
|
|
36
58
|
` decide before ${(0, server_client_1.printable)(run.approvals?.[0]?.expiresAt ?? '-')}`,
|
|
37
59
|
];
|
|
38
60
|
}
|
|
61
|
+
exports.describeApproval = describeApproval;
|
|
39
62
|
// What to look at first, one line.
|
|
40
63
|
function flagLines(run) {
|
|
41
64
|
const flagged = Object.entries(run.plan ?? {}).filter(([, entry]) => entry.flags.length > 0);
|
|
@@ -51,7 +74,11 @@ function flagLines(run) {
|
|
|
51
74
|
function describePlan(plan) {
|
|
52
75
|
return Object.entries(plan).flatMap(([name, entry]) => [
|
|
53
76
|
` ${colors_1.default.bold((0, server_client_1.printable)(name))} ${entry.kind}, ${(0, server_client_1.printable)(entry.runtime)}${entry.image ? ` ${(0, server_client_1.printable)(entry.image)}` : ''}${entry.flags.length > 0 ? ` [${entry.flags.map(server_client_1.printable).join(', ')}]` : ''}`,
|
|
54
|
-
|
|
77
|
+
// a command is what is decided on: shown whole, however long
|
|
78
|
+
...entry.cmds.map((cmd) => ` $ ${(0, server_client_1.printableWhole)(cmd)}`),
|
|
79
|
+
...(entry.network !== undefined && entry.network !== 'open'
|
|
80
|
+
? [` network: ${(0, server_client_1.printable)(typeof entry.network === 'string' ? entry.network : JSON.stringify(entry.network))}`]
|
|
81
|
+
: []),
|
|
55
82
|
...(entry.deps.length > 0 ? [` after: ${entry.deps.map(server_client_1.printable).join(', ')}`] : []),
|
|
56
83
|
...(entry.needs.length > 0 ? [` needs: ${entry.needs.map(server_client_1.printable).join(', ')}`] : []),
|
|
57
84
|
...entry.secrets.map((secret) => ` secret ${(0, server_client_1.printable)(secret.from)}${secret.account ? ` as ${(0, server_client_1.printable)(secret.account)}` : ''} -> ${(0, server_client_1.printable)(secret.target)}`),
|
|
@@ -60,29 +87,78 @@ function describePlan(plan) {
|
|
|
60
87
|
...(entry.generates.length > 0 ? [` generates: ${entry.generates.map(server_client_1.printable).join(', ')}`] : []),
|
|
61
88
|
]);
|
|
62
89
|
}
|
|
90
|
+
exports.describePlan = describePlan;
|
|
63
91
|
function registerWorkCommands(remote, context) {
|
|
64
92
|
const { environment, buildFile, getServer, guarded } = context;
|
|
65
93
|
const connect = async (name) => {
|
|
66
94
|
const chosen = await getServer(name);
|
|
67
|
-
|
|
95
|
+
// a CI job or an agent brings its own token, a person signs in
|
|
96
|
+
const token = await (0, token_source_1.getTokenSource)(environment, chosen.name, chosen.server).token();
|
|
68
97
|
return { ...chosen, token };
|
|
69
98
|
};
|
|
70
99
|
remote
|
|
71
|
-
.command('publish
|
|
72
|
-
.description('publish the build file and the files it includes to a server,
|
|
73
|
-
.
|
|
100
|
+
.command('publish [server]')
|
|
101
|
+
.description('publish the build file and the files it includes to a server, runs refer to it by its hash')
|
|
102
|
+
.addOption(new commander_1.Option('--annotation <key=value>', 'say more about where it comes from, or leave one out with key=; path, repo, commit, machine and user are filled in').argParser(collectOption))
|
|
103
|
+
.action(guarded('Remote publish was not successful', async (name, options) => {
|
|
74
104
|
const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
|
|
105
|
+
const annotations = (0, annotations_1.applyAnnotations)(await (0, annotations_1.collectAnnotations)(buildFile, Object.keys(bundle.files)), options.annotation ?? []);
|
|
75
106
|
const { name: serverName, server, token } = await connect(name);
|
|
76
|
-
const answer = await (0, server_client_1.callServer)(server, token, '
|
|
107
|
+
const answer = await (0, server_client_1.callServer)(server, token, 'POST', '/v1/definitions', {
|
|
108
|
+
...bundle,
|
|
109
|
+
annotations,
|
|
110
|
+
});
|
|
77
111
|
const version = answer.body;
|
|
78
112
|
const files = Object.keys(bundle.files).length;
|
|
79
113
|
const proposed = answer.status === 202;
|
|
80
114
|
const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
|
|
81
|
-
environment.stdout.write(`${verb}
|
|
115
|
+
environment.stdout.write(`${verb} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
|
|
82
116
|
const names = [...(version.tasks ?? []), ...(version.services ?? [])];
|
|
83
117
|
environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
118
|
+
for (const line of annotationLines(version.annotations, 'from')) {
|
|
119
|
+
environment.stdout.write(`${line.trimStart()}\n`);
|
|
120
|
+
}
|
|
84
121
|
if (proposed) {
|
|
85
|
-
environment.stdout.write(`it can not run before somebody who may publish
|
|
122
|
+
environment.stdout.write(`it can not run before somebody who may publish approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
|
|
123
|
+
}
|
|
124
|
+
}));
|
|
125
|
+
remote
|
|
126
|
+
.command('versions [server]')
|
|
127
|
+
.description('list the published versions of build files on a server, newest first, with where they come from')
|
|
128
|
+
.addOption(new commander_1.Option('--filter <key=value>', 'only the versions with this annotation, such as repo=github.com/corp/payments').argParser(collectOption))
|
|
129
|
+
.action(guarded('Remote versions was not successful', async (name, options) => {
|
|
130
|
+
const query = new URLSearchParams();
|
|
131
|
+
for (const filter of (options.filter ?? [])) {
|
|
132
|
+
const index = filter.indexOf('=');
|
|
133
|
+
if (index <= 0) {
|
|
134
|
+
throw new Error(`${(0, server_client_1.printable)(filter)} is not a filter, write it as key=value`);
|
|
135
|
+
}
|
|
136
|
+
if (index === filter.length - 1) {
|
|
137
|
+
throw new Error(`${(0, server_client_1.printable)(filter)} matches nothing, an annotation always has a value`);
|
|
138
|
+
}
|
|
139
|
+
query.append(filter.slice(0, index), filter.slice(index + 1));
|
|
140
|
+
}
|
|
141
|
+
const { name: serverName, server, token } = await connect(name);
|
|
142
|
+
const path = `/v1/definitions${query.toString() !== '' ? `?${query}` : ''}`;
|
|
143
|
+
const { body } = await (0, server_client_1.callServer)(server, token, 'GET', path);
|
|
144
|
+
if (body.definitions.length === 0) {
|
|
145
|
+
environment.stdout.write(`${serverName}: no published versions${query.toString() !== '' ? ' match' : ''}\n`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
environment.stdout.write(`${serverName}: ${body.definitions.length} published\n`);
|
|
149
|
+
for (const version of body.definitions) {
|
|
150
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} published by ${(0, server_client_1.printable)(version.publishedBy.sub)} at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
|
|
151
|
+
for (const line of annotationLines(version.annotations)) {
|
|
152
|
+
environment.stdout.write(`${line}\n`);
|
|
153
|
+
}
|
|
154
|
+
const names = [...version.tasks, ...version.services];
|
|
155
|
+
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
156
|
+
if (version.accounts.length > 0) {
|
|
157
|
+
environment.stdout.write(` accounts: ${version.accounts.map(server_client_1.printable).join(', ')}\n`);
|
|
158
|
+
}
|
|
159
|
+
for (const line of yankLines(version)) {
|
|
160
|
+
environment.stdout.write(`${line}\n`);
|
|
161
|
+
}
|
|
86
162
|
}
|
|
87
163
|
}));
|
|
88
164
|
remote
|
|
@@ -97,21 +173,84 @@ function registerWorkCommands(remote, context) {
|
|
|
97
173
|
}
|
|
98
174
|
environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
|
|
99
175
|
for (const run of body.approvals) {
|
|
100
|
-
for (const line of
|
|
176
|
+
for (const line of describeApproval(run)) {
|
|
101
177
|
environment.stdout.write(`${line}\n`);
|
|
102
178
|
}
|
|
103
179
|
}
|
|
104
180
|
}));
|
|
181
|
+
// A version that is published: who published it, what its tasks do and every file.
|
|
182
|
+
const showPublished = async (hash, serverName, server, token) => {
|
|
183
|
+
const path = `/v1/definitions/${hash}`;
|
|
184
|
+
let version;
|
|
185
|
+
try {
|
|
186
|
+
version = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (e instanceof server_client_1.ServerError && e.status === 404) {
|
|
190
|
+
throw new Error(`${serverName} has no proposal or published version ${(0, server_client_1.printable)(hash)} you may see`);
|
|
191
|
+
}
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
195
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(version.hash))} on ${serverName}, published by ${(0, server_client_1.printable)(version.publishedBy.sub)} (${(0, server_client_1.printable)(version.publishedBy.iss)}) at ${(0, server_client_1.printable)(version.publishedAt)}${version.approvedBy ? `, approved by ${(0, server_client_1.printable)(version.approvedBy.sub)}` : ''}\n`);
|
|
196
|
+
for (const line of [
|
|
197
|
+
...yankLines(version),
|
|
198
|
+
...annotationLines(version.annotations),
|
|
199
|
+
...flagLines(version),
|
|
200
|
+
...(version.plan ? describePlan(version.plan) : [' the server did not record what its tasks do']),
|
|
201
|
+
...(0, bundle_text_1.showBundle)(files),
|
|
202
|
+
]) {
|
|
203
|
+
environment.stdout.write(`${line}\n`);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
105
206
|
remote
|
|
106
|
-
.command('show <
|
|
107
|
-
.description('show a run
|
|
108
|
-
.action(guarded('Remote show was not successful', async (
|
|
109
|
-
if (!RUN_ID.test(
|
|
110
|
-
throw new Error(`${(0, server_client_1.printable)(
|
|
207
|
+
.command('show <target> [server]')
|
|
208
|
+
.description('show a run (run_...) or a proposed or published version (sha256:...) in full: the commands, secrets and accounts of its tasks, and the files of a version')
|
|
209
|
+
.action(guarded('Remote show was not successful', async (target, name) => {
|
|
210
|
+
if (!RUN_ID.test(target) && !bundle_1.HASH_PATTERN.test(target)) {
|
|
211
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a version, list them with: hammerkit remote approvals, remote proposals or remote versions`);
|
|
111
212
|
}
|
|
112
|
-
const { server, token } = await connect(name);
|
|
113
|
-
|
|
114
|
-
|
|
213
|
+
const { name: serverName, server, token } = await connect(name);
|
|
214
|
+
if (bundle_1.HASH_PATTERN.test(target)) {
|
|
215
|
+
const path = `/v1/proposals/${target}`;
|
|
216
|
+
let proposal;
|
|
217
|
+
try {
|
|
218
|
+
proposal = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
if (e instanceof server_client_1.ServerError && e.status === 404) {
|
|
222
|
+
await showPublished(target, serverName, server, token);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
throw e;
|
|
226
|
+
}
|
|
227
|
+
if (proposal.state === 'approved') {
|
|
228
|
+
await showPublished(target, serverName, server, token);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (proposal.state !== 'waiting') {
|
|
232
|
+
// a decided proposal keeps no files
|
|
233
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)}, ${(0, server_client_1.printable)(proposal.state)}${proposal.decidedBy ? ` by ${(0, server_client_1.printable)(proposal.decidedBy.sub)}` : ''}${proposal.reason ? `: ${(0, server_client_1.printable)(proposal.reason)}` : ''}\n`);
|
|
234
|
+
for (const line of annotationLines(proposal.annotations)) {
|
|
235
|
+
environment.stdout.write(`${line}\n`);
|
|
236
|
+
}
|
|
237
|
+
environment.stdout.write(' its files are not kept once it is decided\n');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const files = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
241
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} on ${serverName}, proposed by ${(0, server_client_1.printable)(proposal.proposedBy.sub)} (${(0, server_client_1.printable)(proposal.proposedBy.iss)}), ${(0, server_client_1.printable)(proposal.state)}\n`);
|
|
242
|
+
for (const line of [
|
|
243
|
+
...annotationLines(proposal.annotations),
|
|
244
|
+
...flagLines(proposal),
|
|
245
|
+
...describePlan(proposal.plan ?? {}),
|
|
246
|
+
...(0, bundle_text_1.showBundle)(files),
|
|
247
|
+
]) {
|
|
248
|
+
environment.stdout.write(`${line}\n`);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const view = (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/approvals/${target}`)).body;
|
|
253
|
+
for (const line of describeApproval(view)) {
|
|
115
254
|
environment.stdout.write(`${line}\n`);
|
|
116
255
|
}
|
|
117
256
|
environment.stdout.write(view.plan ? `${describePlan(view.plan).join('\n')}\n` : 'the server did not record what its tasks do\n');
|
|
@@ -130,7 +269,10 @@ function registerWorkCommands(remote, context) {
|
|
|
130
269
|
for (const proposal of body.proposals) {
|
|
131
270
|
const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
|
|
132
271
|
const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
|
|
133
|
-
environment.stdout.write(`${colors_1.default.bold(
|
|
272
|
+
environment.stdout.write(`${colors_1.default.bold((0, server_client_1.printable)(proposal.hash))} proposed by ${who}, ${state}\n`);
|
|
273
|
+
for (const line of annotationLines(proposal.annotations)) {
|
|
274
|
+
environment.stdout.write(`${line}\n`);
|
|
275
|
+
}
|
|
134
276
|
const names = [...proposal.tasks, ...proposal.services];
|
|
135
277
|
environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
|
|
136
278
|
if (proposal.accounts.length > 0) {
|
|
@@ -141,9 +283,6 @@ function registerWorkCommands(remote, context) {
|
|
|
141
283
|
for (const line of flagLines(proposal)) {
|
|
142
284
|
environment.stdout.write(`${line}\n`);
|
|
143
285
|
}
|
|
144
|
-
if (proposal.stale) {
|
|
145
|
-
environment.stdout.write(` stale: ${(0, server_client_1.printable)(proposal.name)} has a newer version, reject it and propose again on top of it\n`);
|
|
146
|
-
}
|
|
147
286
|
if (proposal.state === 'waiting') {
|
|
148
287
|
environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
|
|
149
288
|
}
|
|
@@ -152,43 +291,19 @@ function registerWorkCommands(remote, context) {
|
|
|
152
291
|
}
|
|
153
292
|
}
|
|
154
293
|
}));
|
|
155
|
-
remote
|
|
156
|
-
.command('diff <proposal> [server]')
|
|
157
|
-
.description('show what a proposed version (name@hash) changes against the version it follows')
|
|
158
|
-
.action(guarded('Remote diff was not successful', async (target, name) => {
|
|
159
|
-
const proposal = parseProposal(target);
|
|
160
|
-
if (!proposal) {
|
|
161
|
-
throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
|
|
162
|
-
}
|
|
163
|
-
const { name: serverName, server, token } = await connect(name);
|
|
164
|
-
const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
|
|
165
|
-
const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
|
|
166
|
-
const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
|
|
167
|
-
const base = view.base
|
|
168
|
-
? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
|
|
169
|
-
: undefined;
|
|
170
|
-
environment.stdout.write(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)} on ${serverName}, proposed by ${(0, server_client_1.printable)(view.proposedBy.sub)} (${(0, server_client_1.printable)(view.proposedBy.iss)})\n`);
|
|
171
|
-
environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
|
|
172
|
-
if (view.stale) {
|
|
173
|
-
environment.stdout.write(`warning: ${(0, server_client_1.printable)(proposal.name)} has a newer version (${(0, server_client_1.printable)(view.latest)}), this can only be rejected\n`);
|
|
174
|
-
}
|
|
175
|
-
const lines = (0, text_diff_1.diffBundles)(base, proposed);
|
|
176
|
-
environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
|
|
177
|
-
}));
|
|
178
294
|
for (const action of ['yank', 'unyank']) {
|
|
179
295
|
remote
|
|
180
296
|
.command(`${action} <version> [server]`)
|
|
181
297
|
.description(action === 'yank'
|
|
182
|
-
? 'pull a published version (
|
|
183
|
-
: 'let a yanked version (
|
|
298
|
+
? 'pull a published version (sha256:...) back: no run starts from it'
|
|
299
|
+
: 'let a yanked version (sha256:...) go on')
|
|
184
300
|
.addOption(new commander_1.Option('--reason <text>', 'why, shown to whoever tries to run it').argParser((value) => value))
|
|
185
301
|
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it like payments@sha256:...`);
|
|
302
|
+
if (!bundle_1.HASH_PATTERN.test(target)) {
|
|
303
|
+
throw new Error(`${(0, server_client_1.printable)(target)} is not a version, name it by its hash: sha256:...`);
|
|
189
304
|
}
|
|
190
305
|
const { name: serverName, server, token } = await connect(name);
|
|
191
|
-
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${
|
|
306
|
+
await (0, server_client_1.callServer)(server, token, 'POST', `/v1/definitions/${target}/${action}`, options.reason ? { reason: options.reason } : undefined);
|
|
192
307
|
environment.stdout.write(`${action === 'yank' ? 'yanked' : 'let go on'} ${(0, server_client_1.printable)(target)} on ${serverName}\n`);
|
|
193
308
|
}));
|
|
194
309
|
}
|
|
@@ -196,18 +311,16 @@ function registerWorkCommands(remote, context) {
|
|
|
196
311
|
remote
|
|
197
312
|
.command(`${action} <target> [server]`)
|
|
198
313
|
.description(action === 'approve'
|
|
199
|
-
? 'let a run (run_...) or a proposed version (
|
|
200
|
-
: 'end a run (run_...) or drop a proposed version (
|
|
314
|
+
? 'let a run (run_...) or a proposed version (sha256:...) that waits for your approval go ahead'
|
|
315
|
+
: 'end a run (run_...) or drop a proposed version (sha256:...) that waits for your approval')
|
|
201
316
|
.addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
|
|
202
317
|
.action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
|
|
203
|
-
const proposal =
|
|
318
|
+
const proposal = bundle_1.HASH_PATTERN.test(target);
|
|
204
319
|
if (!proposal && !RUN_ID.test(target)) {
|
|
205
320
|
throw new Error(`${(0, server_client_1.printable)(target)} is not a run id or a proposed version, list them with: hammerkit remote approvals or remote proposals`);
|
|
206
321
|
}
|
|
207
322
|
const { name: serverName, server, token } = await connect(name);
|
|
208
|
-
const path = proposal
|
|
209
|
-
? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
|
|
210
|
-
: `/v1/approvals/${target}/${action}`;
|
|
323
|
+
const path = proposal ? `/v1/proposals/${target}/${action}` : `/v1/approvals/${target}/${action}`;
|
|
211
324
|
const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
|
|
212
325
|
const rejected = body.state === 'rejected';
|
|
213
326
|
environment.stdout.write(proposal
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.callServer = exports.getMe = exports.getDiscovery = exports.printable = exports.ServerError = void 0;
|
|
3
|
+
exports.callServer = exports.getMe = exports.getDiscovery = exports.printableWhole = exports.printable = exports.ServerError = void 0;
|
|
4
4
|
const discovery_1 = require("../server/discovery");
|
|
5
5
|
const TIMEOUT_MS = 10000;
|
|
6
6
|
const WRITE_TIMEOUT_MS = 30000;
|
|
7
7
|
class ServerError extends Error {
|
|
8
|
+
// what the server answered, when it did
|
|
9
|
+
constructor(message, status) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
exports.ServerError = ServerError;
|
|
10
15
|
// Text from the server or the identity provider is shown to the user: no
|
|
11
|
-
// control characters, so it can't rewrite the terminal
|
|
16
|
+
// control characters, so it can't rewrite the terminal, and format characters
|
|
17
|
+
// (bidi overrides, zero-width) written out, so text reads in the order it runs.
|
|
18
|
+
// Long text is cut, and says how much was.
|
|
12
19
|
function printable(value) {
|
|
20
|
+
const text = printableWhole(value);
|
|
21
|
+
return text.length > 300 ? `${text.slice(0, 300)}… (+${text.length - 300} chars)` : text;
|
|
22
|
+
}
|
|
23
|
+
exports.printable = printable;
|
|
24
|
+
// The same, never cut: for what somebody decides on, such as a command.
|
|
25
|
+
function printableWhole(value) {
|
|
13
26
|
return String(value)
|
|
14
27
|
.replace(/\p{Cc}/gu, '?')
|
|
15
|
-
.
|
|
28
|
+
.replace(/\p{Cf}/gu, (char) => `\\u{${char.codePointAt(0)?.toString(16)}}`);
|
|
16
29
|
}
|
|
17
|
-
exports.
|
|
30
|
+
exports.printableWhole = printableWhole;
|
|
18
31
|
async function getJson(url, token) {
|
|
19
32
|
let response;
|
|
20
33
|
try {
|
|
@@ -80,7 +93,7 @@ async function callServer(server, token, method, path, body) {
|
|
|
80
93
|
}
|
|
81
94
|
if (!response.ok) {
|
|
82
95
|
const said = answer && typeof answer.error === 'string' ? `: ${printable(answer.error)}` : '';
|
|
83
|
-
throw new ServerError(`${server.url} answered ${response.status}${said}
|
|
96
|
+
throw new ServerError(`${server.url} answered ${response.status}${said}`, response.status);
|
|
84
97
|
}
|
|
85
98
|
return { status: response.status, body: answer };
|
|
86
99
|
}
|
|
@@ -53,6 +53,16 @@ function hashSource(manifest) {
|
|
|
53
53
|
}
|
|
54
54
|
exports.hashSource = hashSource;
|
|
55
55
|
const DENIED_DIRECTORIES = ['.git', '.ssh', '.aws', '.gnupg', '.kube'];
|
|
56
|
+
// the short names Windows gives these directories, `GIT~1` for `.git`
|
|
57
|
+
const DENIED_SHORT_NAMES = /^(git|ssh|aws|gnupg|kube)~\d+$/;
|
|
58
|
+
// A name as a case-insensitive disk opens it: Windows and macOS open `.SSH` as `.ssh`, and
|
|
59
|
+
// Windows drops trailing dots and spaces (`.git.`) and reads `name:stream` as `name`.
|
|
60
|
+
function canonicalName(name) {
|
|
61
|
+
return name
|
|
62
|
+
.replace(/:.*$/, '')
|
|
63
|
+
.replace(/[. ]+$/, '')
|
|
64
|
+
.toLowerCase();
|
|
65
|
+
}
|
|
56
66
|
const DENIED_NAMES = [
|
|
57
67
|
/^\.env$/i,
|
|
58
68
|
/^\.env\.(?!example$|sample$|template$|dist$)/i,
|
|
@@ -65,12 +75,15 @@ const DENIED_NAMES = [
|
|
|
65
75
|
// must not be what decides that a key leaves the machine.
|
|
66
76
|
function deniedSourcePath(path) {
|
|
67
77
|
const parts = path.split('/');
|
|
68
|
-
const directory = parts.slice(0, -1).find((part) =>
|
|
78
|
+
const directory = parts.slice(0, -1).find((part) => {
|
|
79
|
+
const name = canonicalName(part);
|
|
80
|
+
return DENIED_DIRECTORIES.includes(name) || DENIED_SHORT_NAMES.test(name);
|
|
81
|
+
});
|
|
69
82
|
if (directory !== undefined) {
|
|
70
83
|
return `${directory}/ holds credentials or history`;
|
|
71
84
|
}
|
|
72
85
|
const name = parts[parts.length - 1];
|
|
73
|
-
if (DENIED_NAMES.some((pattern) => pattern.test(name))) {
|
|
86
|
+
if (DENIED_NAMES.some((pattern) => pattern.test(name) || pattern.test(canonicalName(name)))) {
|
|
74
87
|
return `${name} looks like a credential file`;
|
|
75
88
|
}
|
|
76
89
|
return null;
|