hammerkit 1.23.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.
- package/build.schema.json +10 -0
- package/dist/cli.js +6 -0
- package/dist/executer/execute-command.js +7 -1
- package/dist/planner/utils/append-work-task.js +1 -0
- package/dist/program.js +4 -0
- package/dist/remote/bundle.js +59 -0
- package/dist/remote/collect-bundle.js +41 -0
- package/dist/remote/credentials.js +93 -0
- package/dist/remote/device-login.js +177 -0
- package/dist/remote/host-config.js +170 -0
- package/dist/remote/location.js +46 -0
- package/dist/remote/remote-commands.js +154 -0
- package/dist/remote/remote-work-commands.js +152 -0
- package/dist/remote/server-client.js +88 -0
- package/dist/remote/source-manifest.js +79 -0
- package/dist/remote/text-diff.js +89 -0
- package/dist/schema/build-file-base-task-schema.js +4 -0
- package/dist/schema/build-file-schema.js +3 -0
- package/dist/schema/reference-parser.js +1 -0
- package/dist/schema/runner-schema.js +13 -0
- package/dist/server/approvals-api.js +99 -0
- package/dist/server/definitions-api.js +119 -0
- package/dist/server/definitions.js +248 -0
- package/dist/server/discovery.js +9 -0
- package/dist/server/executor.js +238 -0
- package/dist/server/git-checkout.js +63 -0
- package/dist/server/http.js +39 -0
- package/dist/server/oauth.js +226 -0
- package/dist/server/policy.js +90 -0
- package/dist/server/proposals-api.js +123 -0
- package/dist/server/proposals.js +126 -0
- package/dist/server/run-request.js +67 -0
- package/dist/server/runs.js +279 -0
- package/dist/server/server-command.js +43 -0
- package/dist/server/server-config.js +233 -0
- package/dist/server/server.js +338 -0
- package/dist/server/sources-api.js +106 -0
- package/dist/server/sources.js +258 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleApprovals = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const http_1 = require("./http");
|
|
6
|
+
const oauth_1 = require("./oauth");
|
|
7
|
+
const policy_1 = require("./policy");
|
|
8
|
+
const runs_1 = require("./runs");
|
|
9
|
+
const decisionSchema = zod_1.z.object({ reason: zod_1.z.string().max(500).optional() }).strict();
|
|
10
|
+
// What an approver is shown of a run: everything that decides whether to let it
|
|
11
|
+
// go ahead, and who asked.
|
|
12
|
+
function toApprovalView(run) {
|
|
13
|
+
return {
|
|
14
|
+
id: run.id,
|
|
15
|
+
state: run.state,
|
|
16
|
+
requestedBy: run.owner,
|
|
17
|
+
createdAt: run.createdAt,
|
|
18
|
+
definition: run.definition,
|
|
19
|
+
tasks: run.tasks,
|
|
20
|
+
account: run.account,
|
|
21
|
+
source: run.source,
|
|
22
|
+
options: run.options,
|
|
23
|
+
approval: run.approval,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The runs waiting for an approval, which an admin of the service account they
|
|
28
|
+
* use gives:
|
|
29
|
+
*
|
|
30
|
+
* GET /v1/approvals the runs the caller may approve
|
|
31
|
+
* GET /v1/approvals/<id> one of them
|
|
32
|
+
* POST /v1/approvals/<id>/approve let it run (body: optional {reason})
|
|
33
|
+
* POST /v1/approvals/<id>/reject end it
|
|
34
|
+
*
|
|
35
|
+
* What the caller may not approve does not exist for them; their own run is the
|
|
36
|
+
* exception, they are told why it is not theirs to decide.
|
|
37
|
+
*/
|
|
38
|
+
async function handleApprovals(config, registry, log, principal, req, res, url) {
|
|
39
|
+
const match = /^\/v1\/approvals(?:\/([^/]+)(?:\/(approve|reject))?)?$/.exec(url.pathname);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const [, id, action] = match;
|
|
44
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
45
|
+
const canDecide = (account) => (0, policy_1.isAdmin)(grant, account);
|
|
46
|
+
if (!id) {
|
|
47
|
+
if (req.method !== 'GET') {
|
|
48
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
const open = registry
|
|
52
|
+
.waiting()
|
|
53
|
+
.filter((run) => !(run.owner.iss === principal.iss && run.owner.sub === principal.sub) && canDecide(run.account));
|
|
54
|
+
(0, http_1.send)(res, 200, { approvals: open.map(toApprovalView) });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (!runs_1.RUN_ID_PATTERN.test(id)) {
|
|
58
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (!action) {
|
|
62
|
+
if (req.method !== 'GET') {
|
|
63
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
const run = registry.getForApproval(id, canDecide);
|
|
67
|
+
run ? (0, http_1.send)(res, 200, toApprovalView(run)) : (0, http_1.send)(res, 404, { error: 'not found' });
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
if (req.method !== 'POST') {
|
|
71
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const body = decisionSchema.safeParse(req.headers['content-type'] ? await (0, http_1.readJson)(req) : {});
|
|
75
|
+
if (!body.success) {
|
|
76
|
+
const issue = body.error.issues[0];
|
|
77
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
const outcome = action === 'approve' ? 'approved' : 'rejected';
|
|
81
|
+
const result = registry.decide(principal, id, outcome, canDecide, body.data.reason);
|
|
82
|
+
log(`${action} run=${id} iss=${principal.iss} sub=${principal.sub} result=${result.status}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
83
|
+
switch (result.status) {
|
|
84
|
+
case 'ok':
|
|
85
|
+
(0, http_1.send)(res, 200, toApprovalView(result.run));
|
|
86
|
+
break;
|
|
87
|
+
case 'own-run':
|
|
88
|
+
(0, http_1.send)(res, 403, { error: 'a run is approved by somebody else than who asked for it' });
|
|
89
|
+
break;
|
|
90
|
+
case 'not-waiting':
|
|
91
|
+
(0, http_1.send)(res, 409, { error: `the run is ${result.state}, it does not wait for an approval` });
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
exports.handleApprovals = handleApprovals;
|
|
99
|
+
//# sourceMappingURL=approvals-api.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleDefinitions = void 0;
|
|
4
|
+
const bundle_1 = require("../remote/bundle");
|
|
5
|
+
const definitions_1 = require("./definitions");
|
|
6
|
+
const http_1 = require("./http");
|
|
7
|
+
const policy_1 = require("./policy");
|
|
8
|
+
const proposals_1 = require("./proposals");
|
|
9
|
+
const server_config_1 = require("./server-config");
|
|
10
|
+
// A bundle is JSON text with escapes, so the body may be somewhat larger than the files.
|
|
11
|
+
const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
12
|
+
/**
|
|
13
|
+
* The published build files:
|
|
14
|
+
*
|
|
15
|
+
* PUT /v1/definitions/<name> publish a bundle, or propose it when the
|
|
16
|
+
* caller may only propose (answers 202)
|
|
17
|
+
* GET /v1/definitions the names, with their latest version
|
|
18
|
+
* GET /v1/definitions/<name> the versions of one
|
|
19
|
+
* GET /v1/definitions/<name>/<hash|latest> one version
|
|
20
|
+
* GET /v1/definitions/<name>/<hash|latest>/files the files of one version
|
|
21
|
+
*/
|
|
22
|
+
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
23
|
+
const match = /^\/v1\/definitions(?:\/([^/]+)(?:\/([^/]+)(\/files)?)?)?$/.exec(url.pathname);
|
|
24
|
+
if (!match) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const [, name, version, files] = match;
|
|
28
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
29
|
+
if (name !== undefined && !server_config_1.definitionName.safeParse(name).success) {
|
|
30
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (name !== undefined && version === undefined && req.method === 'PUT') {
|
|
34
|
+
await publish(config, store, proposals, log, principal, name, req, res);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (req.method !== 'GET') {
|
|
38
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: name !== undefined && !version ? 'GET, PUT' : 'GET' });
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
// what the caller may not see does not exist for them
|
|
42
|
+
if (!(0, policy_1.maySeeDefinitions)(grant)) {
|
|
43
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (name === undefined) {
|
|
47
|
+
(0, http_1.send)(res, 200, {
|
|
48
|
+
definitions: store.names().map((n) => ({ name: n, latest: store.find(n, 'latest')?.hash })),
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (version === undefined) {
|
|
53
|
+
const versions = store.list(name);
|
|
54
|
+
versions.length > 0
|
|
55
|
+
? (0, http_1.send)(res, 200, { name, latest: versions[0].hash, versions })
|
|
56
|
+
: (0, http_1.send)(res, 404, { error: 'not found' });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const found = version === 'latest' || bundle_1.HASH_PATTERN.test(version) ? store.find(name, version) : undefined;
|
|
60
|
+
if (!found) {
|
|
61
|
+
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (files) {
|
|
65
|
+
const bundle = await store.files(found);
|
|
66
|
+
bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
(0, http_1.send)(res, 200, found);
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
exports.handleDefinitions = handleDefinitions;
|
|
74
|
+
async function publish(config, store, proposals, log, principal, name, req, res) {
|
|
75
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
76
|
+
const direct = (0, policy_1.mayPublish)(grant, name);
|
|
77
|
+
if (!direct && !(0, policy_1.mayPropose)(grant, name)) {
|
|
78
|
+
log(`refuse publish iss=${principal.iss} sub=${principal.sub}: build file ${name} is not granted`);
|
|
79
|
+
(0, http_1.send)(res, 403, { error: `you may not publish ${name}` });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
const issue = parsed.error.issues[0];
|
|
85
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
if (!direct) {
|
|
90
|
+
// a version can't run before it is approved, but it has to load, as it would
|
|
91
|
+
// have when published, so the approver is not asked about a broken one
|
|
92
|
+
const { version, existing } = await store.prepare(name, parsed.data, principal);
|
|
93
|
+
if (existing) {
|
|
94
|
+
(0, http_1.send)(res, 200, { ...existing, created: false });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const { proposal, created } = proposals.propose(version, parsed.data, principal);
|
|
98
|
+
log(`propose ${name} ${proposal.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${proposal.tasks.length}`);
|
|
99
|
+
(0, http_1.send)(res, created ? 202 : 200, { ...proposal, created });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const { version, created } = await store.publish(name, parsed.data, principal);
|
|
103
|
+
log(`publish ${name} ${version.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${version.tasks.length}`);
|
|
104
|
+
(0, http_1.send)(res, created ? 201 : 200, { ...version, created });
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e instanceof definitions_1.DefinitionError) {
|
|
108
|
+
log(`refuse publish iss=${principal.iss} sub=${principal.sub}: ${name} does not load`);
|
|
109
|
+
(0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (e instanceof proposals_1.ProposalError) {
|
|
113
|
+
(0, http_1.send)(res, 503, { error: e.message });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
throw e;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=definitions-api.js.map
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DefinitionStore = exports.inspectBundle = exports.DefinitionError = void 0;
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
const os_1 = require("os");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const yaml_1 = require("yaml");
|
|
9
|
+
const get_file_context_1 = require("../file/get-file-context");
|
|
10
|
+
const resolve_git_source_1 = require("../includes/resolve-git-source");
|
|
11
|
+
const schema_parser_1 = require("../schema/schema-parser");
|
|
12
|
+
const reference_parser_1 = require("../schema/reference-parser");
|
|
13
|
+
const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
14
|
+
const work_task_1 = require("../planner/work-task");
|
|
15
|
+
const bundle_1 = require("../remote/bundle");
|
|
16
|
+
const log_1 = require("../log");
|
|
17
|
+
class DefinitionError extends Error {
|
|
18
|
+
}
|
|
19
|
+
exports.DefinitionError = DefinitionError;
|
|
20
|
+
// 40 (sha-1) or 64 (sha-256) hex digits
|
|
21
|
+
const COMMIT_PATTERN = /^[0-9a-f]{40}([0-9a-f]{24})?$/i;
|
|
22
|
+
// A hash alone can only name what is inside the bundle, so everything a build
|
|
23
|
+
// file pulls in from elsewhere has to be pinned: a git include needs a commit
|
|
24
|
+
// and a remote that is not a path on the server.
|
|
25
|
+
function findLooseIncludes(bundle) {
|
|
26
|
+
const problems = [];
|
|
27
|
+
for (const [path, content] of Object.entries(bundle.files)) {
|
|
28
|
+
if (!/\.ya?ml$/i.test(path)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
let document;
|
|
32
|
+
try {
|
|
33
|
+
document = (0, yaml_1.parse)(content);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
continue; // a file that isn't valid yaml is reported when the bundle is parsed
|
|
37
|
+
}
|
|
38
|
+
for (const block of ['includes', 'references']) {
|
|
39
|
+
for (const [name, include] of Object.entries(document?.[block] ?? {})) {
|
|
40
|
+
const source = include;
|
|
41
|
+
if (!source || typeof source !== 'object' || source.git === undefined) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (typeof source.git !== 'string' || !/^(https:\/\/|ssh:\/\/|git@[\w.-]+:)/.test(source.git)) {
|
|
45
|
+
problems.push(`${path}: ${block}.${name}: git has to be an https:// or ssh:// address`);
|
|
46
|
+
}
|
|
47
|
+
if (typeof source.ref !== 'string' || !COMMIT_PATTERN.test(source.ref)) {
|
|
48
|
+
problems.push(`${path}: ${block}.${name}: pin ref to a commit, a branch or tag can move`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return problems;
|
|
54
|
+
}
|
|
55
|
+
// What a build file may read while it is inspected: its own files. The rest of
|
|
56
|
+
// the server is not its business (a missing file is "not there", not an error).
|
|
57
|
+
function confined(files, root) {
|
|
58
|
+
const check = (path) => {
|
|
59
|
+
if (!(0, resolve_git_source_1.isWithin)(root, (0, path_1.resolve)(root, path))) {
|
|
60
|
+
throw new DefinitionError(`${path} is outside of the bundle`);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const guarded = (fn) => (path) => {
|
|
64
|
+
check(path);
|
|
65
|
+
return fn.call(files, path);
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
...files,
|
|
69
|
+
read: guarded(files.read),
|
|
70
|
+
readStream: guarded(files.readStream),
|
|
71
|
+
listFiles: guarded(files.listFiles),
|
|
72
|
+
stats: guarded(files.stats),
|
|
73
|
+
exists: async (path) => (0, resolve_git_source_1.isWithin)(root, (0, path_1.resolve)(root, path)) && files.exists(path),
|
|
74
|
+
watch: files.watch,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// A path of a task, written relative to the root of the bundle with forward
|
|
78
|
+
// slashes. A leading `!` (an exclusion) is kept. Nothing is returned for a path
|
|
79
|
+
// outside of the bundle.
|
|
80
|
+
function insideBundle(root, cwd, path) {
|
|
81
|
+
const negated = path.startsWith('!');
|
|
82
|
+
const absolute = (0, path_1.resolve)(cwd, negated ? path.slice(1) : path);
|
|
83
|
+
const inside = (0, path_1.relative)(root, absolute);
|
|
84
|
+
if (inside === '..' || inside.startsWith(`..${path_1.sep}`) || (0, path_1.isAbsolute)(inside)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const portable = inside.split(path_1.sep).join(path_1.posix.sep);
|
|
88
|
+
// a pattern keeps its glob characters, `resolve` only normalizes separators
|
|
89
|
+
return [`${negated ? '!' : ''}${portable === '' ? '.' : portable}`];
|
|
90
|
+
}
|
|
91
|
+
// Writes the bundle into a directory of its own and reads it the way a run
|
|
92
|
+
// would, so a build file that doesn't parse is refused when it is published, not
|
|
93
|
+
// when somebody wants to run it.
|
|
94
|
+
async function inspectBundle(bundle, environment) {
|
|
95
|
+
const problems = findLooseIncludes(bundle);
|
|
96
|
+
if (problems.length > 0) {
|
|
97
|
+
throw new DefinitionError(problems.join('\n'));
|
|
98
|
+
}
|
|
99
|
+
const directory = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'hammerkit-definition-'));
|
|
100
|
+
try {
|
|
101
|
+
for (const [path, content] of Object.entries(bundle.files)) {
|
|
102
|
+
await (0, promises_1.mkdir)((0, path_1.dirname)((0, path_1.join)(directory, path)), { recursive: true });
|
|
103
|
+
await (0, promises_1.writeFile)((0, path_1.join)(directory, path), content);
|
|
104
|
+
}
|
|
105
|
+
const inspected = {
|
|
106
|
+
...environment,
|
|
107
|
+
cwd: directory,
|
|
108
|
+
file: confined((0, get_file_context_1.getFileContext)(directory), directory),
|
|
109
|
+
};
|
|
110
|
+
try {
|
|
111
|
+
const { ctx, scope } = await (0, schema_parser_1.createParseContext)((0, path_1.join)(directory, bundle.entry), inspected);
|
|
112
|
+
const context = await (0, reference_parser_1.parseReferences)(ctx, scope, inspected);
|
|
113
|
+
const workTree = (0, work_scope_parser_1.getWorkContext)(context, { environmentName: null }, inspected);
|
|
114
|
+
const inputs = {};
|
|
115
|
+
for (const item of Object.values(workTree.tasks)) {
|
|
116
|
+
inputs[item.name] = {
|
|
117
|
+
// the sources a task inherits from its dependencies belong to those
|
|
118
|
+
src: item.data.src
|
|
119
|
+
.filter((source) => source.inherited === null)
|
|
120
|
+
.flatMap((source) => insideBundle(directory, item.data.cwd, source.source)),
|
|
121
|
+
mounts: (0, work_task_1.isContainerWorkTask)(item.data)
|
|
122
|
+
? item.data.mounts.flatMap((mount) => insideBundle(directory, item.data.cwd, mount.localPath))
|
|
123
|
+
: [],
|
|
124
|
+
deps: item.deps.map((dep) => dep.name).sort(),
|
|
125
|
+
needs: item.needs.map((need) => need.name).sort(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
tasks: Object.keys(context.tasks).sort(),
|
|
130
|
+
services: Object.keys(context.services).sort(),
|
|
131
|
+
inputs: Object.fromEntries(Object.entries(inputs).sort(([a], [b]) => (a < b ? -1 : 1))),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
throw new DefinitionError((0, log_1.getErrorMessage)(e).split(directory).join('.'));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
await (0, promises_1.rm)(directory, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.inspectBundle = inspectBundle;
|
|
143
|
+
// The published build files: immutable versions, each named by its hash. They
|
|
144
|
+
// are kept in files, so a restart doesn't forget what runs were started from.
|
|
145
|
+
class DefinitionStore {
|
|
146
|
+
constructor(directory, environment, now = Date.now) {
|
|
147
|
+
this.directory = directory;
|
|
148
|
+
this.environment = environment;
|
|
149
|
+
this.now = now;
|
|
150
|
+
this.versions = new Map();
|
|
151
|
+
}
|
|
152
|
+
async load() {
|
|
153
|
+
let names = [];
|
|
154
|
+
try {
|
|
155
|
+
names = await (0, promises_1.readdir)(this.directory);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return; // nothing was published yet
|
|
159
|
+
}
|
|
160
|
+
for (const name of names) {
|
|
161
|
+
for (const file of await (0, promises_1.readdir)((0, path_1.join)(this.directory, name))) {
|
|
162
|
+
if (!file.endsWith('.json')) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const stored = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(this.directory, name, file), 'utf8'));
|
|
166
|
+
this.add(stored.version);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
names() {
|
|
171
|
+
return [...this.versions.keys()].sort();
|
|
172
|
+
}
|
|
173
|
+
// newest first
|
|
174
|
+
list(name) {
|
|
175
|
+
return this.has(name) ? [...this.versions.get(name)] : [];
|
|
176
|
+
}
|
|
177
|
+
// a hash, or `latest` for the version published last
|
|
178
|
+
find(name, hash) {
|
|
179
|
+
const versions = this.list(name);
|
|
180
|
+
return hash === 'latest' ? versions[0] : versions.find((v) => v.hash === hash);
|
|
181
|
+
}
|
|
182
|
+
async files(version) {
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(await (0, promises_1.readFile)(this.file(version.name, version.hash), 'utf8')).bundle;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Reads a bundle the way a run would and says what it would be as a version;
|
|
191
|
+
// `existing` is the published version with the same hash, if there is one.
|
|
192
|
+
async prepare(name, bundle, publisher) {
|
|
193
|
+
const hash = (0, bundle_1.hashBundle)(bundle);
|
|
194
|
+
const existing = this.find(name, hash);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return { version: existing, existing };
|
|
197
|
+
}
|
|
198
|
+
const { tasks, services, inputs } = await inspectBundle(bundle, this.environment);
|
|
199
|
+
return {
|
|
200
|
+
version: {
|
|
201
|
+
name,
|
|
202
|
+
hash,
|
|
203
|
+
entry: bundle.entry,
|
|
204
|
+
publishedAt: new Date(this.now()).toISOString(),
|
|
205
|
+
publishedBy: { iss: publisher.iss, sub: publisher.sub },
|
|
206
|
+
tasks,
|
|
207
|
+
services,
|
|
208
|
+
inputs,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Publishing what already is there changes nothing and says so.
|
|
213
|
+
async publish(name, bundle, publisher, approvedBy) {
|
|
214
|
+
const prepared = await this.prepare(name, bundle, publisher);
|
|
215
|
+
if (prepared.existing) {
|
|
216
|
+
return { version: prepared.existing, created: false };
|
|
217
|
+
}
|
|
218
|
+
const version = {
|
|
219
|
+
...prepared.version,
|
|
220
|
+
publishedAt: new Date(this.now()).toISOString(),
|
|
221
|
+
...(approvedBy ? { approvedBy: { iss: approvedBy.iss, sub: approvedBy.sub } } : {}),
|
|
222
|
+
};
|
|
223
|
+
const file = this.file(name, version.hash);
|
|
224
|
+
await (0, promises_1.mkdir)((0, path_1.dirname)(file), { recursive: true });
|
|
225
|
+
const temporary = `${file}.${(0, crypto_1.randomBytes)(8).toString('hex')}.tmp`;
|
|
226
|
+
await (0, promises_1.writeFile)(temporary, JSON.stringify({ version, bundle }), { flag: 'wx' });
|
|
227
|
+
await (0, promises_1.rename)(temporary, file);
|
|
228
|
+
this.add(version);
|
|
229
|
+
return { version, created: true };
|
|
230
|
+
}
|
|
231
|
+
has(name) {
|
|
232
|
+
return this.versions.has(name);
|
|
233
|
+
}
|
|
234
|
+
add(version) {
|
|
235
|
+
const versions = this.versions.get(version.name) ?? [];
|
|
236
|
+
versions.unshift(version);
|
|
237
|
+
versions.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
|
|
238
|
+
this.versions.set(version.name, versions);
|
|
239
|
+
}
|
|
240
|
+
file(name, hash) {
|
|
241
|
+
if (!bundle_1.HASH_PATTERN.test(hash)) {
|
|
242
|
+
throw new DefinitionError('invalid hash');
|
|
243
|
+
}
|
|
244
|
+
return (0, path_1.join)(this.directory, name, `${hash.slice('sha256:'.length)}.json`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
exports.DefinitionStore = DefinitionStore;
|
|
248
|
+
//# sourceMappingURL=definitions.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DISCOVERY_PATH = exports.SERVER_PROTOCOL = void 0;
|
|
4
|
+
// What a client needs to know about a server before it signs in. Kept apart from
|
|
5
|
+
// the server so the client does not load the http server and token code.
|
|
6
|
+
// Bumped when a client could no longer talk to an older or newer server.
|
|
7
|
+
exports.SERVER_PROTOCOL = 1;
|
|
8
|
+
exports.DISCOVERY_PATH = '/.well-known/hammerkit';
|
|
9
|
+
//# sourceMappingURL=discovery.js.map
|