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
|
@@ -8,45 +8,43 @@ const http_1 = require("./http");
|
|
|
8
8
|
const oauth_1 = require("./oauth");
|
|
9
9
|
const policy_1 = require("./policy");
|
|
10
10
|
const proposals_1 = require("./proposals");
|
|
11
|
-
const server_config_1 = require("./server-config");
|
|
12
11
|
// A bundle is JSON text with escapes, so the body may be somewhat larger than the files.
|
|
13
12
|
const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
14
13
|
/**
|
|
15
|
-
* The published build files:
|
|
14
|
+
* The published build files, each version named by its hash:
|
|
16
15
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* GET /v1/definitions
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* POST /v1/definitions publish a bundle, or propose it when the caller
|
|
17
|
+
* may only propose (answers 202); answers its hash.
|
|
18
|
+
* Beside entry and files the body may carry
|
|
19
|
+
* `annotations`, which are not part of the hash
|
|
20
|
+
* GET /v1/definitions the versions, newest first; `?<key>=<value>`
|
|
21
|
+
* keeps the ones with those annotations
|
|
22
|
+
* GET /v1/definitions/<hash> one version
|
|
23
|
+
* GET /v1/definitions/<hash>/files its files
|
|
24
|
+
* POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
|
|
25
|
+
* POST /v1/definitions/<hash>/unyank let it go on
|
|
25
26
|
*/
|
|
26
27
|
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
27
|
-
const match = /^\/v1\/definitions(?:\/([^/]+)(
|
|
28
|
+
const match = /^\/v1\/definitions(?:\/([^/]+)(\/files|\/yank|\/unyank)?)?$/.exec(url.pathname);
|
|
28
29
|
if (!match) {
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
31
|
-
const [,
|
|
32
|
-
const files = suffix === '/files';
|
|
32
|
+
const [, hash, suffix] = match;
|
|
33
33
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
34
|
-
if (
|
|
34
|
+
if (hash !== undefined && !bundle_1.HASH_PATTERN.test(hash)) {
|
|
35
35
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
-
if (
|
|
39
|
-
await publish(config, store, proposals, log, principal,
|
|
38
|
+
if (hash === undefined && req.method === 'POST') {
|
|
39
|
+
await publish(config, store, proposals, log, principal, req, res);
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
42
|
if ((suffix === '/yank' || suffix === '/unyank') && req.method === 'POST') {
|
|
43
|
-
await yank(config, store, log, principal,
|
|
43
|
+
await yank(config, store, log, principal, hash, suffix === '/unyank', req, res);
|
|
44
44
|
return true;
|
|
45
45
|
}
|
|
46
46
|
if (req.method !== 'GET') {
|
|
47
|
-
(0, http_1.send)(res, 405, { error: 'method not allowed' }, {
|
|
48
|
-
allow: suffix === '/yank' || suffix === '/unyank' ? 'POST' : name !== undefined && !version ? 'GET, PUT' : 'GET',
|
|
49
|
-
});
|
|
47
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: suffix === '/yank' || suffix === '/unyank' ? 'POST' : hash === undefined ? 'GET, POST' : 'GET' });
|
|
50
48
|
return true;
|
|
51
49
|
}
|
|
52
50
|
// what the caller may not see does not exist for them
|
|
@@ -54,25 +52,22 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
|
|
|
54
52
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
55
53
|
return true;
|
|
56
54
|
}
|
|
57
|
-
if (
|
|
55
|
+
if (hash === undefined) {
|
|
56
|
+
const wanted = [...url.searchParams.entries()];
|
|
58
57
|
(0, http_1.send)(res, 200, {
|
|
59
|
-
definitions: store
|
|
58
|
+
definitions: store
|
|
59
|
+
.list()
|
|
60
|
+
.filter((version) => wanted.every(([key, value]) => version.annotations?.[key] === value))
|
|
61
|
+
.map(toSummary),
|
|
60
62
|
});
|
|
61
63
|
return true;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
const versions = store.list(name);
|
|
65
|
-
versions.length > 0
|
|
66
|
-
? (0, http_1.send)(res, 200, { name, latest: versions[0].hash, versions })
|
|
67
|
-
: (0, http_1.send)(res, 404, { error: 'not found' });
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
const found = version === 'latest' || bundle_1.HASH_PATTERN.test(version) ? store.find(name, version) : undefined;
|
|
65
|
+
const found = store.find(hash);
|
|
71
66
|
if (!found) {
|
|
72
67
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
73
68
|
return true;
|
|
74
69
|
}
|
|
75
|
-
if (files) {
|
|
70
|
+
if (suffix === '/files') {
|
|
76
71
|
const bundle = await store.files(found);
|
|
77
72
|
bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
|
|
78
73
|
}
|
|
@@ -82,22 +77,38 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
|
|
|
82
77
|
return true;
|
|
83
78
|
}
|
|
84
79
|
exports.handleDefinitions = handleDefinitions;
|
|
80
|
+
// What the list shows of a version: who published it and when, and what it runs as.
|
|
81
|
+
function toSummary(version) {
|
|
82
|
+
const { hash, entry, publishedAt, publishedBy, approvedBy, annotations, tasks, services, accounts, yanked } = version;
|
|
83
|
+
return {
|
|
84
|
+
hash,
|
|
85
|
+
entry,
|
|
86
|
+
publishedAt,
|
|
87
|
+
publishedBy,
|
|
88
|
+
approvedBy,
|
|
89
|
+
annotations: annotations ?? {},
|
|
90
|
+
tasks,
|
|
91
|
+
services,
|
|
92
|
+
accounts: accounts ?? [],
|
|
93
|
+
yanked,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
85
96
|
const yankSchema = zod_1.z.object({ reason: zod_1.z.string().max(500).optional() }).strict();
|
|
86
97
|
// Yanking is allowed to who publishes the build file or uses an account the version
|
|
87
98
|
// uses; letting a version go on again is the publisher's call.
|
|
88
|
-
async function yank(config, store, log, principal,
|
|
99
|
+
async function yank(config, store, log, principal, hash, undo, req, res) {
|
|
89
100
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
90
|
-
const version =
|
|
101
|
+
const version = store.find(hash);
|
|
91
102
|
if (!version || !(0, policy_1.maySeeDefinitions)(grant)) {
|
|
92
103
|
(0, http_1.send)(res, 404, { error: 'not found' });
|
|
93
104
|
return;
|
|
94
105
|
}
|
|
95
106
|
if (undo ? !(0, policy_1.mayPublish)(grant) : !(0, policy_1.mayYank)(grant, version)) {
|
|
96
|
-
log(`refuse ${undo ? 'unyank' : 'yank'} ${
|
|
107
|
+
log(`refuse ${undo ? 'unyank' : 'yank'} ${hash} iss=${principal.iss} sub=${principal.sub}`);
|
|
97
108
|
(0, http_1.send)(res, 403, {
|
|
98
109
|
error: undo
|
|
99
|
-
? 'a version is let go on by who publishes
|
|
100
|
-
: 'a version is yanked by who publishes
|
|
110
|
+
? 'a version is let go on by who publishes build files'
|
|
111
|
+
: 'a version is yanked by who publishes build files or uses an account it uses',
|
|
101
112
|
});
|
|
102
113
|
return;
|
|
103
114
|
}
|
|
@@ -107,18 +118,28 @@ async function yank(config, store, log, principal, name, hash, undo, req, res) {
|
|
|
107
118
|
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
108
119
|
return;
|
|
109
120
|
}
|
|
110
|
-
const changed = await store.yank(
|
|
111
|
-
log(`${undo ? 'unyank' : 'yank'} ${
|
|
121
|
+
const changed = await store.yank(hash, principal, { reason: body.data.reason, undo });
|
|
122
|
+
log(`${undo ? 'unyank' : 'yank'} ${hash} iss=${principal.iss} sub=${principal.sub}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
112
123
|
(0, http_1.send)(res, 200, changed);
|
|
113
124
|
}
|
|
114
|
-
async function publish(config, store, proposals, log, principal,
|
|
125
|
+
async function publish(config, store, proposals, log, principal, req, res) {
|
|
115
126
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
116
127
|
if (!(0, policy_1.mayPublish)(grant) && !(0, policy_1.mayPropose)(grant)) {
|
|
117
128
|
log(`refuse publish iss=${principal.iss} sub=${principal.sub}: no builds permission`);
|
|
118
|
-
(0, http_1.send)(res, 403, { error:
|
|
129
|
+
(0, http_1.send)(res, 403, { error: 'you may not publish build files, it needs builds: propose or publish' });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const body = await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES);
|
|
133
|
+
// the annotations ride along with the bundle, they are not part of it
|
|
134
|
+
const { annotations: said, ...rest } = body && typeof body === 'object' ? body : {};
|
|
135
|
+
const annotated = bundle_1.annotationsSchema.optional().safeParse(said);
|
|
136
|
+
if (!annotated.success) {
|
|
137
|
+
const issue = annotated.error.issues[0];
|
|
138
|
+
(0, http_1.send)(res, 400, { error: `${['annotations', ...issue.path].join('.')}: ${issue.message}` });
|
|
119
139
|
return;
|
|
120
140
|
}
|
|
121
|
-
const
|
|
141
|
+
const annotations = annotated.data;
|
|
142
|
+
const parsed = bundle_1.bundleSchema.safeParse(rest);
|
|
122
143
|
if (!parsed.success) {
|
|
123
144
|
const issue = parsed.error.issues[0];
|
|
124
145
|
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
@@ -127,8 +148,9 @@ async function publish(config, store, proposals, log, principal, name, req, res)
|
|
|
127
148
|
try {
|
|
128
149
|
// a version can't run before it is approved, but it has to load, as it would
|
|
129
150
|
// have when published, so the approver is not asked about a broken one
|
|
130
|
-
const { version, existing } = await store.prepare(
|
|
151
|
+
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
131
152
|
noRemote: !(0, policy_1.mayPublish)(grant),
|
|
153
|
+
annotations,
|
|
132
154
|
});
|
|
133
155
|
if (existing) {
|
|
134
156
|
(0, http_1.send)(res, 200, { ...existing, created: false });
|
|
@@ -138,18 +160,18 @@ async function publish(config, store, proposals, log, principal, name, req, res)
|
|
|
138
160
|
// else proposes it, and somebody who does approves it (four eyes)
|
|
139
161
|
const missing = (version.accounts ?? []).filter((account) => !(0, policy_1.canUse)(grant, account));
|
|
140
162
|
if (!(0, policy_1.mayPublish)(grant) || missing.length > 0) {
|
|
141
|
-
const { proposal, created } = proposals.propose(version, parsed.data, principal, missing
|
|
142
|
-
log(`propose ${
|
|
163
|
+
const { proposal, created } = proposals.propose(version, parsed.data, principal, missing);
|
|
164
|
+
log(`propose ${proposal.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${proposal.tasks.length} missing=[${missing.join(',')}]`);
|
|
143
165
|
(0, http_1.send)(res, created ? 202 : 200, { ...proposal, created });
|
|
144
166
|
return;
|
|
145
167
|
}
|
|
146
|
-
const { version: published, created } = await store.publish(
|
|
147
|
-
log(`publish ${
|
|
168
|
+
const { version: published, created } = await store.publish(parsed.data, principal, { annotations });
|
|
169
|
+
log(`publish ${published.hash} iss=${principal.iss} sub=${principal.sub} created=${created} tasks=${published.tasks.length}`);
|
|
148
170
|
(0, http_1.send)(res, created ? 201 : 200, { ...published, created });
|
|
149
171
|
}
|
|
150
172
|
catch (e) {
|
|
151
173
|
if (e instanceof definitions_1.DefinitionError) {
|
|
152
|
-
log(`refuse publish iss=${principal.iss} sub=${principal.sub}:
|
|
174
|
+
log(`refuse publish iss=${principal.iss} sub=${principal.sub}: the build file does not load`);
|
|
153
175
|
(0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
|
|
154
176
|
return;
|
|
155
177
|
}
|
|
@@ -9,8 +9,10 @@ const plan_1 = require("./plan");
|
|
|
9
9
|
var plan_2 = require("./plan");
|
|
10
10
|
Object.defineProperty(exports, "DefinitionError", { enumerable: true, get: function () { return plan_2.DefinitionError; } });
|
|
11
11
|
Object.defineProperty(exports, "inspectBundle", { enumerable: true, get: function () { return plan_2.inspectBundle; } });
|
|
12
|
-
// The published build files: immutable versions, each named by its hash
|
|
13
|
-
//
|
|
12
|
+
// The published build files: immutable versions, each named by its hash and by
|
|
13
|
+
// nothing else, so whoever publishes can't make one look like the follower of
|
|
14
|
+
// another. They are kept in files, so a restart doesn't forget what runs were
|
|
15
|
+
// started from.
|
|
14
16
|
class DefinitionStore {
|
|
15
17
|
constructor(directory,
|
|
16
18
|
// the environment a build file is read with: on a server, the hosted one
|
|
@@ -22,42 +24,43 @@ class DefinitionStore {
|
|
|
22
24
|
this.now = options.now ?? Date.now;
|
|
23
25
|
}
|
|
24
26
|
async load() {
|
|
25
|
-
let
|
|
27
|
+
let entries = [];
|
|
26
28
|
try {
|
|
27
|
-
|
|
29
|
+
entries = await (0, promises_1.readdir)(this.directory);
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
return; // nothing was published yet
|
|
31
33
|
}
|
|
32
|
-
for (const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const path = (0, path_1.join)(this.directory, entry);
|
|
36
|
+
if ((await (0, promises_1.stat)(path)).isDirectory()) {
|
|
37
|
+
// a name of an older server: its versions are versions like any other
|
|
38
|
+
for (const file of await (0, promises_1.readdir)(path)) {
|
|
39
|
+
await this.read((0, path_1.join)(path, file), true);
|
|
36
40
|
}
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await this.read(path, false);
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
|
-
names() {
|
|
43
|
-
return [...this.versions.keys()].sort();
|
|
44
|
-
}
|
|
45
47
|
// newest first
|
|
46
|
-
list(
|
|
47
|
-
return
|
|
48
|
+
list() {
|
|
49
|
+
return [...this.versions.values()]
|
|
50
|
+
.map((stored) => stored.version)
|
|
51
|
+
.sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
|
|
48
52
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const versions = this.list(name);
|
|
52
|
-
return hash === 'latest' ? versions.find((v) => !v.yanked) : versions.find((v) => v.hash === hash);
|
|
53
|
+
find(hash) {
|
|
54
|
+
return this.versions.get(hash)?.version;
|
|
53
55
|
}
|
|
54
56
|
/**
|
|
55
57
|
* Pulls a version back (or, with `undo`, lets it go on). The version is rewritten in
|
|
56
58
|
* place, its files stay: a run that was started from it has them.
|
|
57
59
|
*/
|
|
58
|
-
async yank(
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
async yank(hash, by, options = {}) {
|
|
61
|
+
const stored = this.versions.get(hash);
|
|
62
|
+
const version = stored?.version;
|
|
63
|
+
if (!stored || !version || Boolean(version.yanked) === !options.undo) {
|
|
61
64
|
return version;
|
|
62
65
|
}
|
|
63
66
|
const bundle = await this.files(version);
|
|
@@ -75,18 +78,21 @@ class DefinitionStore {
|
|
|
75
78
|
...(options.reason ? { reason: options.reason } : {}),
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
await (
|
|
81
|
+
// a version of an older server is written as `<hex>.json`, which counts over the
|
|
82
|
+
// copies it may have under several names
|
|
83
|
+
const file = this.fileOf(hash);
|
|
84
|
+
await this.write(file, next, bundle);
|
|
85
|
+
stored.file = file;
|
|
86
|
+
stored.legacy = false;
|
|
82
87
|
// the same object, so whoever holds the version sees it
|
|
83
88
|
Object.keys(version).forEach((key) => delete version[key]);
|
|
84
89
|
Object.assign(version, next);
|
|
85
90
|
return version;
|
|
86
91
|
}
|
|
87
92
|
async files(version) {
|
|
93
|
+
const stored = this.versions.get(version.hash);
|
|
88
94
|
try {
|
|
89
|
-
return JSON.parse(await (0, promises_1.readFile)(
|
|
95
|
+
return stored ? JSON.parse(await (0, promises_1.readFile)(stored.file, 'utf8')).bundle : undefined;
|
|
90
96
|
}
|
|
91
97
|
catch {
|
|
92
98
|
return undefined;
|
|
@@ -94,11 +100,11 @@ class DefinitionStore {
|
|
|
94
100
|
}
|
|
95
101
|
// Reads a bundle the way a run would and says what it would be as a version;
|
|
96
102
|
// `existing` is the published version with the same hash, if there is one.
|
|
97
|
-
async prepare(
|
|
103
|
+
async prepare(bundle, publisher,
|
|
98
104
|
// a proposal is read before anybody trusts it: it may not make the server fetch
|
|
99
105
|
options = {}) {
|
|
100
106
|
const hash = (0, bundle_1.hashBundle)(bundle);
|
|
101
|
-
const existing = this.find(
|
|
107
|
+
const existing = this.find(hash);
|
|
102
108
|
if (existing) {
|
|
103
109
|
return { version: existing, existing };
|
|
104
110
|
}
|
|
@@ -108,11 +114,13 @@ class DefinitionStore {
|
|
|
108
114
|
});
|
|
109
115
|
return {
|
|
110
116
|
version: {
|
|
111
|
-
name,
|
|
112
117
|
hash,
|
|
113
118
|
entry: bundle.entry,
|
|
114
119
|
publishedAt: new Date(this.now()).toISOString(),
|
|
115
120
|
publishedBy: { iss: publisher.iss, sub: publisher.sub },
|
|
121
|
+
...(options.annotations && Object.keys(options.annotations).length > 0
|
|
122
|
+
? { annotations: { ...options.annotations } }
|
|
123
|
+
: {}),
|
|
116
124
|
tasks,
|
|
117
125
|
services,
|
|
118
126
|
inputs,
|
|
@@ -121,9 +129,11 @@ class DefinitionStore {
|
|
|
121
129
|
},
|
|
122
130
|
};
|
|
123
131
|
}
|
|
124
|
-
// Publishing what already is there changes nothing and says so
|
|
125
|
-
|
|
126
|
-
|
|
132
|
+
// Publishing what already is there changes nothing and says so, its annotations
|
|
133
|
+
// included: they stay those of the first publish.
|
|
134
|
+
async publish(bundle, publisher, options = {}) {
|
|
135
|
+
const { approvedBy } = options;
|
|
136
|
+
const prepared = await this.prepare(bundle, publisher, { annotations: options.annotations });
|
|
127
137
|
if (prepared.existing) {
|
|
128
138
|
return { version: prepared.existing, created: false };
|
|
129
139
|
}
|
|
@@ -132,28 +142,43 @@ class DefinitionStore {
|
|
|
132
142
|
publishedAt: new Date(this.now()).toISOString(),
|
|
133
143
|
...(approvedBy ? { approvedBy: { iss: approvedBy.iss, sub: approvedBy.sub } } : {}),
|
|
134
144
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
await (0, promises_1.
|
|
140
|
-
this.
|
|
145
|
+
if (!bundle_1.HASH_PATTERN.test(version.hash)) {
|
|
146
|
+
throw new plan_1.DefinitionError('invalid hash');
|
|
147
|
+
}
|
|
148
|
+
const file = this.fileOf(version.hash);
|
|
149
|
+
await (0, promises_1.mkdir)(this.directory, { recursive: true });
|
|
150
|
+
await this.write(file, version, bundle);
|
|
151
|
+
this.versions.set(version.hash, { version, file, legacy: false });
|
|
141
152
|
return { version, created: true };
|
|
142
153
|
}
|
|
143
|
-
|
|
144
|
-
return this.
|
|
154
|
+
fileOf(hash) {
|
|
155
|
+
return (0, path_1.join)(this.directory, `${hash.slice('sha256:'.length)}.json`);
|
|
145
156
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
async read(file, legacy) {
|
|
158
|
+
if (!file.endsWith('.json')) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const { version } = JSON.parse(await (0, promises_1.readFile)(file, 'utf8'));
|
|
162
|
+
// the name an older server kept with it means nothing now
|
|
163
|
+
delete version.name;
|
|
164
|
+
const stored = this.versions.get(version.hash);
|
|
165
|
+
if (stored && !stored.legacy) {
|
|
166
|
+
return; // `<hex>.json` is what was written last
|
|
167
|
+
}
|
|
168
|
+
// published under two names, it is still one version: yanked under either name, it
|
|
169
|
+
// stays yanked, else the first one counts
|
|
170
|
+
const known = stored?.version;
|
|
171
|
+
const first = !legacy ||
|
|
172
|
+
!known ||
|
|
173
|
+
(Boolean(version.yanked) === Boolean(known.yanked) && version.publishedAt < known.publishedAt);
|
|
174
|
+
if (first || (version.yanked && !known?.yanked)) {
|
|
175
|
+
this.versions.set(version.hash, { version, file, legacy });
|
|
155
176
|
}
|
|
156
|
-
|
|
177
|
+
}
|
|
178
|
+
async write(file, version, bundle) {
|
|
179
|
+
const temporary = `${file}.${(0, crypto_1.randomBytes)(8).toString('hex')}.tmp`;
|
|
180
|
+
await (0, promises_1.writeFile)(temporary, JSON.stringify({ version, bundle }), { flag: 'wx' });
|
|
181
|
+
await (0, promises_1.rename)(temporary, file);
|
|
157
182
|
}
|
|
158
183
|
}
|
|
159
184
|
exports.DefinitionStore = DefinitionStore;
|