hammerkit 1.22.0 → 1.24.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 (54) hide show
  1. package/build.schema.json +92 -2
  2. package/dist/cli.js +25 -0
  3. package/dist/executer/environment-mock.js +4 -1
  4. package/dist/executer/execute-command.js +7 -1
  5. package/dist/index.js +4 -1
  6. package/dist/planner/service-definition.js +3 -1
  7. package/dist/planner/utils/append-work-service.js +1 -1
  8. package/dist/planner/utils/append-work-task.js +2 -1
  9. package/dist/planner/work-item-status.js +3 -2
  10. package/dist/planner/work-secret.js +87 -10
  11. package/dist/program.js +4 -0
  12. package/dist/remote/bundle.js +59 -0
  13. package/dist/remote/collect-bundle.js +41 -0
  14. package/dist/remote/credentials.js +93 -0
  15. package/dist/remote/device-login.js +177 -0
  16. package/dist/remote/host-config.js +170 -0
  17. package/dist/remote/location.js +46 -0
  18. package/dist/remote/remote-commands.js +154 -0
  19. package/dist/remote/remote-work-commands.js +152 -0
  20. package/dist/remote/server-client.js +88 -0
  21. package/dist/remote/source-manifest.js +79 -0
  22. package/dist/remote/text-diff.js +89 -0
  23. package/dist/schema/build-file-base-task-schema.js +6 -0
  24. package/dist/schema/build-file-container-service-schema.js +2 -0
  25. package/dist/schema/build-file-schema.js +6 -0
  26. package/dist/schema/build-file-secret-provider-schema.js +36 -0
  27. package/dist/schema/build-file-secret-schema.js +6 -3
  28. package/dist/schema/reference-parser.js +4 -0
  29. package/dist/schema/runner-schema.js +13 -0
  30. package/dist/secrets/command-secret-provider.js +108 -0
  31. package/dist/secrets/prefetch-secrets.js +27 -0
  32. package/dist/secrets/provider-values.js +47 -0
  33. package/dist/secrets/secret-catalog.js +78 -0
  34. package/dist/secrets/secret-provider.js +19 -0
  35. package/dist/server/approvals-api.js +99 -0
  36. package/dist/server/definitions-api.js +119 -0
  37. package/dist/server/definitions.js +248 -0
  38. package/dist/server/discovery.js +9 -0
  39. package/dist/server/executor.js +238 -0
  40. package/dist/server/git-checkout.js +63 -0
  41. package/dist/server/http.js +39 -0
  42. package/dist/server/oauth.js +226 -0
  43. package/dist/server/policy.js +90 -0
  44. package/dist/server/proposals-api.js +123 -0
  45. package/dist/server/proposals.js +126 -0
  46. package/dist/server/run-request.js +67 -0
  47. package/dist/server/runs.js +279 -0
  48. package/dist/server/server-command.js +43 -0
  49. package/dist/server/server-config.js +233 -0
  50. package/dist/server/server.js +338 -0
  51. package/dist/server/sources-api.js +106 -0
  52. package/dist/server/sources.js +258 -0
  53. package/dist/utils/redact.js +75 -19
  54. package/package.json +1 -1
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerWorkCommands = void 0;
7
+ const commander_1 = require("commander");
8
+ const colors_1 = __importDefault(require("colors"));
9
+ const credentials_1 = require("./credentials");
10
+ const collect_bundle_1 = require("./collect-bundle");
11
+ const bundle_1 = require("./bundle");
12
+ const server_client_1 = require("./server-client");
13
+ const text_diff_1 = require("./text-diff");
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
+ }
20
+ const RUN_ID = /^run_[A-Za-z0-9_-]{16}$/;
21
+ // What a person who approves is shown of a run: everything the decision rests on.
22
+ function describe(run) {
23
+ const source = run.source.type === 'ref' ? `ref ${run.source.ref}` : `snapshot ${run.source.snapshot}`;
24
+ const options = Object.entries(run.options)
25
+ .map(([key, value]) => `${key}=${Array.isArray(value) ? value.join(',') : value}`)
26
+ .join(' ');
27
+ return [
28
+ `${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
+ ` ${(0, server_client_1.printable)(run.definition.name)} ${(0, server_client_1.printable)(run.definition.hash)}`,
30
+ ` tasks: ${run.tasks.length > 0 ? run.tasks.map(server_client_1.printable).join(', ') : 'selected by labels'}`,
31
+ ` account ${run.account === null ? 'none' : (0, server_client_1.printable)(run.account)}, source ${(0, server_client_1.printable)(source)}${options ? `, ${(0, server_client_1.printable)(options)}` : ''}`,
32
+ ` needs the approval of an admin of ${run.account === null ? 'none' : (0, server_client_1.printable)(run.account)}`,
33
+ ` decide before ${(0, server_client_1.printable)(run.approval?.expiresAt ?? '-')}`,
34
+ ];
35
+ }
36
+ function registerWorkCommands(remote, context) {
37
+ const { environment, buildFile, getServer, guarded } = context;
38
+ const connect = async (name) => {
39
+ const chosen = await getServer(name);
40
+ const token = await (0, credentials_1.getAccessToken)(environment, chosen.name, chosen.server);
41
+ return { ...chosen, token };
42
+ };
43
+ remote
44
+ .command('publish <definition> [server]')
45
+ .description('publish the build file and the files it includes to a server, under a name runs can refer to')
46
+ .action(guarded('Remote publish was not successful', async (definition, name) => {
47
+ const bundle = await (0, collect_bundle_1.collectBundle)(buildFile, environment);
48
+ const { name: serverName, server, token } = await connect(name);
49
+ const answer = await (0, server_client_1.callServer)(server, token, 'PUT', `/v1/definitions/${encodeURIComponent(definition)}`, bundle);
50
+ const version = answer.body;
51
+ const files = Object.keys(bundle.files).length;
52
+ const proposed = answer.status === 202;
53
+ const verb = proposed ? 'proposed' : answer.status === 201 ? 'published' : 'already published';
54
+ environment.stdout.write(`${verb} ${definition} to ${serverName}: ${(0, server_client_1.printable)(version.hash)} (${files} ${files === 1 ? 'file' : 'files'})\n`);
55
+ const names = [...(version.tasks ?? []), ...(version.services ?? [])];
56
+ environment.stdout.write(`tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
57
+ if (proposed) {
58
+ environment.stdout.write(`it can not run before somebody who may publish ${(0, server_client_1.printable)(definition)} approves it, until ${(0, server_client_1.printable)(version.expiresAt)}\n`);
59
+ }
60
+ }));
61
+ remote
62
+ .command('approvals [server]')
63
+ .description('list the runs on a server that wait for your approval')
64
+ .action(guarded('Remote approvals was not successful', async (name) => {
65
+ const { name: serverName, server, token } = await connect(name);
66
+ const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/approvals');
67
+ if (body.approvals.length === 0) {
68
+ environment.stdout.write(`${serverName}: no runs wait for your approval\n`);
69
+ return;
70
+ }
71
+ environment.stdout.write(`${serverName}: ${body.approvals.length} waiting for your approval\n`);
72
+ for (const run of body.approvals) {
73
+ for (const line of describe(run)) {
74
+ environment.stdout.write(`${line}\n`);
75
+ }
76
+ }
77
+ }));
78
+ remote
79
+ .command('proposals [server]')
80
+ .description('list the proposed versions of build files you made or may approve')
81
+ .action(guarded('Remote proposals was not successful', async (name) => {
82
+ const { name: serverName, server, token } = await connect(name);
83
+ const { body } = await (0, server_client_1.callServer)(server, token, 'GET', '/v1/proposals');
84
+ if (body.proposals.length === 0) {
85
+ environment.stdout.write(`${serverName}: no proposed versions\n`);
86
+ return;
87
+ }
88
+ environment.stdout.write(`${serverName}: ${body.proposals.length} proposed\n`);
89
+ for (const proposal of body.proposals) {
90
+ const who = proposal.mine ? 'you' : (0, server_client_1.printable)(proposal.proposedBy.sub);
91
+ const state = proposal.canDecide ? colors_1.default.bold('waits for your approval') : (0, server_client_1.printable)(proposal.state);
92
+ environment.stdout.write(`${colors_1.default.bold(`${(0, server_client_1.printable)(proposal.name)}@${(0, server_client_1.printable)(proposal.hash)}`)} proposed by ${who}, ${state}\n`);
93
+ const names = [...proposal.tasks, ...proposal.services];
94
+ environment.stdout.write(` tasks and services: ${names.map(server_client_1.printable).join(', ') || 'none'}\n`);
95
+ if (proposal.state === 'waiting') {
96
+ environment.stdout.write(` decide before ${(0, server_client_1.printable)(proposal.expiresAt)}\n`);
97
+ }
98
+ else if (proposal.reason) {
99
+ environment.stdout.write(` ${(0, server_client_1.printable)(proposal.state)}: ${(0, server_client_1.printable)(proposal.reason)}\n`);
100
+ }
101
+ }
102
+ }));
103
+ remote
104
+ .command('diff <proposal> [server]')
105
+ .description('show what a proposed version (name@hash) changes against the version it follows')
106
+ .action(guarded('Remote diff was not successful', async (target, name) => {
107
+ const proposal = parseProposal(target);
108
+ if (!proposal) {
109
+ throw new Error(`${(0, server_client_1.printable)(target)} is not a proposal, list them with: hammerkit remote proposals`);
110
+ }
111
+ const { name: serverName, server, token } = await connect(name);
112
+ const path = `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}`;
113
+ const view = (await (0, server_client_1.callServer)(server, token, 'GET', path)).body;
114
+ const proposed = (await (0, server_client_1.callServer)(server, token, 'GET', `${path}/files`)).body;
115
+ const base = view.base
116
+ ? (await (0, server_client_1.callServer)(server, token, 'GET', `/v1/definitions/${encodeURIComponent(proposal.name)}/${view.base}/files`)).body
117
+ : undefined;
118
+ 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`);
119
+ environment.stdout.write(base ? `compared with ${(0, server_client_1.printable)(view.base)}\n` : 'no version is published yet, everything is new\n');
120
+ const lines = (0, text_diff_1.diffBundles)(base, proposed);
121
+ environment.stdout.write(lines.length > 0 ? `${lines.join('\n')}\n` : 'the files are the same\n');
122
+ }));
123
+ for (const action of ['approve', 'reject']) {
124
+ remote
125
+ .command(`${action} <target> [server]`)
126
+ .description(action === 'approve'
127
+ ? 'let a run (run_...) or a proposed version (name@hash) that waits for your approval go ahead'
128
+ : 'end a run (run_...) or drop a proposed version (name@hash) that waits for your approval')
129
+ .addOption(new commander_1.Option('--reason <text>', 'why, shown to who asked').argParser((value) => value))
130
+ .action(guarded(`Remote ${action} was not successful`, async (target, name, options) => {
131
+ const proposal = parseProposal(target);
132
+ if (!proposal && !RUN_ID.test(target)) {
133
+ 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`);
134
+ }
135
+ const { name: serverName, server, token } = await connect(name);
136
+ const path = proposal
137
+ ? `/v1/proposals/${encodeURIComponent(proposal.name)}/${proposal.hash}/${action}`
138
+ : `/v1/approvals/${target}/${action}`;
139
+ const { body } = await (0, server_client_1.callServer)(server, token, 'POST', path, options.reason ? { reason: options.reason } : undefined);
140
+ const rejected = body.state === 'rejected';
141
+ environment.stdout.write(proposal
142
+ ? rejected
143
+ ? `rejected ${target} on ${serverName}\n`
144
+ : `approved ${target} on ${serverName}, it is published\n`
145
+ : rejected
146
+ ? `rejected ${target} on ${serverName}\n`
147
+ : `approved ${target} on ${serverName}, it is queued\n`);
148
+ }));
149
+ }
150
+ }
151
+ exports.registerWorkCommands = registerWorkCommands;
152
+ //# sourceMappingURL=remote-work-commands.js.map
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callServer = exports.getMe = exports.getDiscovery = exports.printable = exports.ServerError = void 0;
4
+ const discovery_1 = require("../server/discovery");
5
+ const TIMEOUT_MS = 10000;
6
+ const WRITE_TIMEOUT_MS = 30000;
7
+ class ServerError extends Error {
8
+ }
9
+ exports.ServerError = ServerError;
10
+ // Text from the server or the identity provider is shown to the user: no
11
+ // control characters, so it can't rewrite the terminal.
12
+ function printable(value) {
13
+ return String(value)
14
+ .replace(/\p{Cc}/gu, '?')
15
+ .slice(0, 300);
16
+ }
17
+ exports.printable = printable;
18
+ async function getJson(url, token) {
19
+ let response;
20
+ try {
21
+ response = await fetch(url, {
22
+ headers: token ? { authorization: `Bearer ${token}` } : {},
23
+ signal: AbortSignal.timeout(TIMEOUT_MS),
24
+ });
25
+ }
26
+ catch (e) {
27
+ throw new ServerError(`unable to reach ${url}: ${e instanceof Error ? e.message : String(e)}`);
28
+ }
29
+ if (response.status === 401) {
30
+ throw new ServerError(`${url} did not accept the token`);
31
+ }
32
+ if (!response.ok) {
33
+ throw new ServerError(`${url} answered ${response.status}`);
34
+ }
35
+ return response.json();
36
+ }
37
+ // What the server says it is. The issuer registered with `remote add` has to be
38
+ // one it accepts, otherwise a login would send the user to an identity provider
39
+ // the server does not trust.
40
+ async function getDiscovery(server) {
41
+ const document = (await getJson(`${server.url}${discovery_1.DISCOVERY_PATH}`));
42
+ if (document?.name !== 'hammerkit' || !Array.isArray(document.issuers)) {
43
+ throw new ServerError(`${server.url} is not a hammerkit server`);
44
+ }
45
+ return document;
46
+ }
47
+ exports.getDiscovery = getDiscovery;
48
+ async function getMe(server, token) {
49
+ return (await getJson(`${server.url}/v1/me`, token));
50
+ }
51
+ exports.getMe = getMe;
52
+ // A call that changes something, or whose answer the caller tells apart by status.
53
+ // Anything but 2xx is an error carrying what the server said about it.
54
+ async function callServer(server, token, method, path, body) {
55
+ const url = `${server.url}${path}`;
56
+ let response;
57
+ try {
58
+ response = await fetch(url, {
59
+ method,
60
+ headers: {
61
+ authorization: `Bearer ${token}`,
62
+ ...(body !== undefined ? { 'content-type': 'application/json' } : {}),
63
+ },
64
+ body: body === undefined ? undefined : JSON.stringify(body),
65
+ signal: AbortSignal.timeout(method === 'GET' ? TIMEOUT_MS : WRITE_TIMEOUT_MS),
66
+ });
67
+ }
68
+ catch (e) {
69
+ throw new ServerError(`unable to reach ${url}: ${printable(e instanceof Error ? e.message : e)}`);
70
+ }
71
+ let answer;
72
+ try {
73
+ answer = await response.json();
74
+ }
75
+ catch {
76
+ answer = undefined;
77
+ }
78
+ if (response.status === 401) {
79
+ throw new ServerError(`${server.url} did not accept the token`);
80
+ }
81
+ if (!response.ok) {
82
+ const said = answer && typeof answer.error === 'string' ? `: ${printable(answer.error)}` : '';
83
+ throw new ServerError(`${server.url} answered ${response.status}${said}`);
84
+ }
85
+ return { status: response.status, body: answer };
86
+ }
87
+ exports.callServer = callServer;
88
+ //# sourceMappingURL=server-client.js.map
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deniedSourcePath = exports.hashSource = exports.sourceManifestSchema = exports.DIGEST_PATTERN = exports.MAX_MANIFEST_BODY_BYTES = exports.MAX_SOURCE_BYTES = exports.MAX_SOURCE_FILE_BYTES = exports.MAX_SOURCE_FILES = void 0;
4
+ const crypto_1 = require("crypto");
5
+ const zod_1 = require("zod");
6
+ const bundle_1 = require("./bundle");
7
+ exports.MAX_SOURCE_FILES = 20000;
8
+ exports.MAX_SOURCE_FILE_BYTES = 32 * 1024 * 1024;
9
+ exports.MAX_SOURCE_BYTES = 1024 * 1024 * 1024;
10
+ // a manifest is about 200 bytes per file
11
+ exports.MAX_MANIFEST_BODY_BYTES = 8 * 1024 * 1024;
12
+ exports.DIGEST_PATTERN = /^[0-9a-f]{64}$/;
13
+ const sourceFileSchema = zod_1.z
14
+ .object({
15
+ path: zod_1.z.string(),
16
+ sha256: zod_1.z.string().regex(exports.DIGEST_PATTERN, 'expected 64 lowercase hex digits'),
17
+ size: zod_1.z.number().int().min(0).max(exports.MAX_SOURCE_FILE_BYTES),
18
+ mode: zod_1.z.enum(['file', 'exec']).default('file'),
19
+ })
20
+ .strict();
21
+ exports.sourceManifestSchema = zod_1.z
22
+ .object({ files: zod_1.z.array(sourceFileSchema).min(1).max(exports.MAX_SOURCE_FILES) })
23
+ .strict()
24
+ .superRefine((manifest, ctx) => {
25
+ const seen = new Set();
26
+ let bytes = 0;
27
+ manifest.files.forEach((file, index) => {
28
+ if (!(0, bundle_1.isBundlePath)(file.path)) {
29
+ ctx.addIssue({
30
+ code: zod_1.z.ZodIssueCode.custom,
31
+ path: ['files', index, 'path'],
32
+ message: `"${file.path}" is not a path inside the project`,
33
+ });
34
+ }
35
+ if (seen.has(file.path)) {
36
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['files', index, 'path'], message: `${file.path} twice` });
37
+ }
38
+ seen.add(file.path);
39
+ bytes += file.size;
40
+ });
41
+ if (bytes > exports.MAX_SOURCE_BYTES) {
42
+ ctx.addIssue({ code: zod_1.z.ZodIssueCode.custom, path: ['files'], message: `more than ${exports.MAX_SOURCE_BYTES} bytes` });
43
+ }
44
+ });
45
+ // The name of a tree: the sha-256 of the JSON `[[path, sha-256, mode], ...]` in
46
+ // path order. The same files have the same name wherever it is computed, and the
47
+ // size is not part of it because the content digest already decides it.
48
+ function hashSource(manifest) {
49
+ const files = [...manifest.files]
50
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
51
+ .map((file) => [file.path, file.sha256, file.mode]);
52
+ return `sha256:${(0, crypto_1.createHash)('sha256').update(JSON.stringify(files)).digest('hex')}`;
53
+ }
54
+ exports.hashSource = hashSource;
55
+ const DENIED_DIRECTORIES = ['.git', '.ssh', '.aws', '.gnupg', '.kube'];
56
+ const DENIED_NAMES = [
57
+ /^\.env$/i,
58
+ /^\.env\.(?!example$|sample$|template$|dist$)/i,
59
+ /^\.(npmrc|netrc|pypirc|dockercfg|git-credentials)$/i,
60
+ /^id_(rsa|dsa|ecdsa|ed25519)$/i,
61
+ /\.(pem|p12|pfx)$/i,
62
+ ];
63
+ // Files that hold credentials are not sent to a server even when a `src` pattern
64
+ // reaches them: a secret belongs in a secret provider, and a glob such as `**`
65
+ // must not be what decides that a key leaves the machine.
66
+ function deniedSourcePath(path) {
67
+ const parts = path.split('/');
68
+ const directory = parts.slice(0, -1).find((part) => DENIED_DIRECTORIES.includes(part));
69
+ if (directory !== undefined) {
70
+ return `${directory}/ holds credentials or history`;
71
+ }
72
+ const name = parts[parts.length - 1];
73
+ if (DENIED_NAMES.some((pattern) => pattern.test(name))) {
74
+ return `${name} looks like a credential file`;
75
+ }
76
+ return null;
77
+ }
78
+ exports.deniedSourcePath = deniedSourcePath;
79
+ //# sourceMappingURL=source-manifest.js.map
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.diffBundles = exports.diffLines = 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
+ // Beyond this many comparisons a file is shown whole instead of compared.
13
+ const MAX_CELLS = 4000000;
14
+ // The lines that differ, with `context` lines around each change.
15
+ function diffLines(before, after, context = 2) {
16
+ if ((before.length + 1) * (after.length + 1) > MAX_CELLS) {
17
+ return undefined;
18
+ }
19
+ const n = before.length;
20
+ const m = after.length;
21
+ // lengths of the longest common suffixes
22
+ const table = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1));
23
+ for (let i = n - 1; i >= 0; i--) {
24
+ for (let j = m - 1; j >= 0; j--) {
25
+ table[i][j] = before[i] === after[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
26
+ }
27
+ }
28
+ const all = [];
29
+ let i = 0;
30
+ let j = 0;
31
+ while (i < n && j < m) {
32
+ if (before[i] === after[j]) {
33
+ all.push({ type: ' ', text: before[i] });
34
+ i++;
35
+ j++;
36
+ }
37
+ else if (table[i + 1][j] >= table[i][j + 1]) {
38
+ all.push({ type: '-', text: before[i++] });
39
+ }
40
+ else {
41
+ all.push({ type: '+', text: after[j++] });
42
+ }
43
+ }
44
+ while (i < n)
45
+ all.push({ type: '-', text: before[i++] });
46
+ while (j < m)
47
+ all.push({ type: '+', text: after[j++] });
48
+ const keep = new Array(all.length).fill(false);
49
+ all.forEach((line, index) => {
50
+ if (line.type !== ' ') {
51
+ for (let k = Math.max(0, index - context); k <= Math.min(all.length - 1, index + context); k++) {
52
+ keep[k] = true;
53
+ }
54
+ }
55
+ });
56
+ return all.filter((_, index) => keep[index]);
57
+ }
58
+ exports.diffLines = diffLines;
59
+ const lines = (text) => (text === '' ? [] : text.replace(/\n$/, '').split('\n'));
60
+ // What a proposal changes against the version it follows, file by file.
61
+ function diffBundles(base, proposed) {
62
+ const out = [];
63
+ const before = base?.files ?? {};
64
+ const paths = [...new Set([...Object.keys(before), ...Object.keys(proposed.files)])].sort();
65
+ for (const path of paths) {
66
+ const old = before[path];
67
+ const next = proposed.files[path];
68
+ if (old === next) {
69
+ continue;
70
+ }
71
+ const status = old === undefined ? 'added' : next === undefined ? 'removed' : 'changed';
72
+ out.push(`--- ${visible(path)} (${status})`);
73
+ const compared = old !== undefined && next !== undefined ? diffLines(lines(old), lines(next)) : undefined;
74
+ if (compared) {
75
+ compared.forEach((line) => out.push(`${line.type} ${visible(line.text)}`));
76
+ }
77
+ else {
78
+ // added, removed or too large to compare: all of the old, then all of the new
79
+ lines(old ?? '').forEach((line) => out.push(`- ${visible(line)}`));
80
+ lines(next ?? '').forEach((line) => out.push(`+ ${visible(line)}`));
81
+ }
82
+ }
83
+ if (base && base.entry !== proposed.entry) {
84
+ out.unshift(`entry file: ${visible(base.entry)} -> ${visible(proposed.entry)}`);
85
+ }
86
+ return out;
87
+ }
88
+ exports.diffBundles = diffBundles;
89
+ //# sourceMappingURL=text-diff.js.map
@@ -11,6 +11,7 @@ const build_file_volume_schema_1 = require("./build-file-volume-schema");
11
11
  const duration_schema_1 = require("./duration-schema");
12
12
  const build_file_secret_schema_1 = require("./build-file-secret-schema");
13
13
  const resources_schema_1 = require("./resources-schema");
14
+ const runner_schema_1 = require("./runner-schema");
14
15
  // what `extend` adds to (the rest, e.g. `image` or `shell`, the task just overrides)
15
16
  exports.RESET_PROPERTIES = ['deps', 'needs', 'src', 'generates', 'cmds', 'mounts', 'envs', 'labels'];
16
17
  exports.buildFileBaseTaskSchema = (0, zod_1.object)({
@@ -34,7 +35,12 @@ exports.buildFileBaseTaskSchema = (0, zod_1.object)({
34
35
  // runs longer. Does not affect the cache key (ADR-0002).
35
36
  timeout: duration_schema_1.durationSchema.optional(),
36
37
  secrets: (0, zod_1.array)(build_file_secret_schema_1.buildFileSecretSchema).optional(),
38
+ // the service account its provider secrets are read as, unless a secret names one
39
+ account: (0, zod_1.string)().optional(),
37
40
  // CPU and memory limits of the container; a local task is not limited
38
41
  resources: resources_schema_1.resourcesSchema.optional(),
42
+ // Name of the server (see `hammerkit remote`) the task has to run on; defaults
43
+ // to the build file's `runner`. Never a local fallback when it is unknown.
44
+ runner: runner_schema_1.runnerSchema.optional(),
39
45
  });
40
46
  //# sourceMappingURL=build-file-base-task-schema.js.map
@@ -36,6 +36,8 @@ exports.buildFileContainerServiceSchema = (0, zod_1.object)({
36
36
  // passed; the service counts as ready for dependents only after it succeeded
37
37
  init: (0, zod_1.string)().describe('task run once the service is healthy, before dependents start').optional(),
38
38
  secrets: (0, zod_1.array)(build_file_secret_schema_1.buildFileSecretSchema).optional(),
39
+ // the service account its provider secrets are read as, unless a secret names one
40
+ account: (0, zod_1.string)().optional(),
39
41
  resources: resources_schema_1.resourcesSchema.optional(),
40
42
  })
41
43
  .strict()
@@ -9,6 +9,8 @@ const build_file_task_schema_1 = require("./build-file-task-schema");
9
9
  const labels_schema_1 = require("./labels-schema");
10
10
  const cache_schema_1 = require("./cache-schema");
11
11
  const build_file_include_schema_1 = require("./build-file-include-schema");
12
+ const build_file_secret_provider_schema_1 = require("./build-file-secret-provider-schema");
13
+ const runner_schema_1 = require("./runner-schema");
12
14
  exports.buildFileSchema = (0, zod_1.object)({
13
15
  envs: envs_schema_1.envsSchema.optional(),
14
16
  tasks: (0, zod_1.record)(build_file_task_schema_1.buildFileTaskSchema).optional(),
@@ -17,7 +19,11 @@ exports.buildFileSchema = (0, zod_1.object)({
17
19
  includes: (0, zod_1.record)(build_file_include_schema_1.buildFileIncludeSchema).optional(),
18
20
  environments: (0, zod_1.record)(build_file_environment_schema_1.buildFileEnvironmentSchema).optional(),
19
21
  caches: (0, zod_1.record)(cache_schema_1.buildFileCacheSchema).optional(),
22
+ secretProviders: build_file_secret_provider_schema_1.buildFileSecretProvidersSchema.optional(),
23
+ secretAccounts: build_file_secret_provider_schema_1.buildFileSecretAccountsSchema.optional(),
20
24
  labels: labels_schema_1.labelsSchema.optional(),
25
+ // default `runner` of the tasks of this file
26
+ runner: runner_schema_1.runnerSchema.optional(),
21
27
  })
22
28
  .strict()
23
29
  .describe('Build file with support for containerization\nhttps://hammerkit.dev/docs/build-file');
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildFileSecretAccountsSchema = exports.buildFileSecretProvidersSchema = exports.buildFileSecretAccountSchema = exports.buildFileSecretProviderSchema = exports.RESERVED_SECRET_PROVIDERS = void 0;
4
+ const zod_1 = require("zod");
5
+ const duration_schema_1 = require("./duration-schema");
6
+ // `env` and `file` are the built-in sources of `from`, so a provider cannot take
7
+ // either name.
8
+ exports.RESERVED_SECRET_PROVIDERS = ['env', 'file'];
9
+ const nameSchema = (0, zod_1.string)()
10
+ .regex(/^[A-Za-z][A-Za-z0-9_-]*$/, 'a name starts with a letter and holds letters, digits, - and _')
11
+ .refine((name) => !exports.RESERVED_SECRET_PROVIDERS.includes(name), 'env and file are built-in secret sources');
12
+ // How to fetch a secret: a command that prints the value to stdout (ADR 0008).
13
+ // The command is an argv list, never run through a shell. `{{ref}}` is replaced
14
+ // by the text after `provider:` in a secret's `from`. It holds no credential:
15
+ // those come from the account the secret is read as.
16
+ exports.buildFileSecretProviderSchema = (0, zod_1.object)({
17
+ command: (0, zod_1.array)((0, zod_1.string)())
18
+ .min(1)
19
+ .describe('command and arguments, no shell; {{ref}} is replaced by the secret reference'),
20
+ env: (0, zod_1.record)((0, zod_1.string)()).optional().describe('static environment of the command, next to PATH and the account env'),
21
+ timeout: duration_schema_1.durationSchema.optional().describe('how long the command may run, 30s when omitted'),
22
+ })
23
+ .strict()
24
+ .describe('secret provider, a command that prints the secret to stdout');
25
+ // As whom a provider is read: the environment its command runs with. Values
26
+ // are `${HOST_VARIABLE}` references to a credential the machine running
27
+ // hammerkit holds, so a build file never carries one.
28
+ exports.buildFileSecretAccountSchema = (0, zod_1.object)({
29
+ default: (0, zod_1.boolean)().optional().describe('the account for secrets and tasks that name none'),
30
+ providers: (0, zod_1.record)(nameSchema, (0, zod_1.object)({ env: (0, zod_1.record)((0, zod_1.string)()) }).strict()).describe('per provider, the environment that makes its command act as this account'),
31
+ })
32
+ .strict()
33
+ .describe('service account the secret providers are read as');
34
+ exports.buildFileSecretProvidersSchema = (0, zod_1.record)(nameSchema, exports.buildFileSecretProviderSchema);
35
+ exports.buildFileSecretAccountsSchema = (0, zod_1.record)(nameSchema, exports.buildFileSecretAccountSchema);
36
+ //# sourceMappingURL=build-file-secret-provider-schema.js.map
@@ -3,11 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildFileSecretSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  // A credential handed to a task or service without putting it in the build
6
- // file, the cache or the logs: `from: env:NAME` or `from: file:path`, injected
6
+ // file, the cache or the logs: `from: env:NAME`, `from: file:path` or
7
+ // `from: <provider>:<ref>` (a secret provider, read as an account), injected
7
8
  // as an env variable (`env`) or a read-only file (`path`). `cache: true` makes
8
9
  // a salted digest of the value part of the task's cache key (ADR 0002).
9
10
  exports.buildFileSecretSchema = (0, zod_1.object)({
10
- from: (0, zod_1.string)().regex(/^(env|file):.+$/, 'from must be env:NAME or file:path'),
11
+ from: (0, zod_1.string)().regex(/^[A-Za-z][A-Za-z0-9_-]*:.+$/, 'from must be env:NAME, file:path or <provider>:<ref>'),
12
+ // the service account a provider secret is read as, else the task's or service's, else the default
13
+ account: (0, zod_1.string)().optional(),
11
14
  env: (0, zod_1.string)()
12
15
  .regex(/^[A-Za-z_][A-Za-z0-9_]*$/)
13
16
  .optional(),
@@ -18,5 +21,5 @@ exports.buildFileSecretSchema = (0, zod_1.object)({
18
21
  .refine((secret) => (secret.env === undefined) !== (secret.path === undefined), {
19
22
  message: 'a secret needs exactly one of env or path',
20
23
  })
21
- .describe('secret from env:NAME or file:path, injected as env or a read-only file');
24
+ .describe('secret from env:NAME, file:path or <provider>:<ref>, injected as env or a read-only file');
22
25
  //# sourceMappingURL=build-file-secret-schema.js.map
@@ -9,6 +9,7 @@ const label_values_1 = require("../executer/label-values");
9
9
  const merge_environment_variables_1 = require("../environment/merge-environment-variables");
10
10
  const resolve_cache_1 = require("../cache/resolve-cache");
11
11
  const project_root_1 = require("../planner/project-root");
12
+ const secret_catalog_1 = require("../secrets/secret-catalog");
12
13
  function combinePath(namePrefix, name) {
13
14
  return namePrefix.length > 0 ? `${namePrefix}:${name}` : name;
14
15
  }
@@ -21,6 +22,7 @@ async function parseReferences(ctx, mainScope, environment) {
21
22
  envFiles: {},
22
23
  environments: {},
23
24
  caches: {},
25
+ secrets: (0, secret_catalog_1.emptySecretCatalog)(),
24
26
  projectRoot: await (0, project_root_1.findProjectRoot)(mainScope.cwd, environment),
25
27
  };
26
28
  for (const file of Object.values(ctx.files)) {
@@ -90,6 +92,7 @@ async function parseReferences(ctx, mainScope, environment) {
90
92
  resolveInit(reference, service);
91
93
  }
92
94
  reference.caches = (0, resolve_cache_1.withBuiltinCaches)(declaredCaches);
95
+ reference.secrets = (0, secret_catalog_1.collectSecretCatalog)(Object.values(ctx.files), environment, reference);
93
96
  return reference;
94
97
  }
95
98
  exports.parseReferences = parseReferences;
@@ -118,6 +121,7 @@ function applySchemaExtension(reference, task) {
118
121
  task.schema.shell = task.schema.shell ?? extend.schema.shell;
119
122
  task.schema.cache = task.schema.cache ?? extend.schema.cache;
120
123
  task.schema.timeout = task.schema.timeout ?? extend.schema.timeout;
124
+ task.schema.runner = task.schema.runner ?? extend.schema.runner;
121
125
  if (!reset.has('cmds'))
122
126
  task.schema.cmds = extendArray(task.schema.cmds, extend.schema.cmds);
123
127
  if (!reset.has('src'))
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runnerSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ const host_config_1 = require("../remote/host-config");
6
+ // A server registered with `hammerkit remote add`; resolved on the machine that
7
+ // runs the build, so the build file carries no URL.
8
+ exports.runnerSchema = (0, zod_1.string)()
9
+ .min(1)
10
+ .refine((name) => !host_config_1.RESERVED_SERVER_NAMES.includes(name), {
11
+ message: 'local and auto are no servers, leave runner out to run where --on or the host config says',
12
+ });
13
+ //# sourceMappingURL=runner-schema.js.map