hammerkit 1.27.0-next.9 → 2.0.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 +176 -6
- package/dist/cache/backends/local-cache-backend.js +6 -0
- package/dist/cache/backends/registry/registry-client.js +1 -0
- package/dist/cache/cache-sync.js +1 -1
- package/dist/cache/explain.js +17 -0
- package/dist/cli.js +2 -0
- package/dist/dashboard/assets/index-CjnKm5-3.css +2 -0
- package/dist/dashboard/assets/index-zPXa4klZ.js +13 -0
- package/dist/dashboard/favicon.svg +1 -0
- package/dist/dashboard/index.html +15 -0
- package/dist/docker/item-network.js +13 -5
- package/dist/executer/archive-task-entry.js +4 -2
- package/dist/executer/deadline.js +17 -3
- package/dist/executer/deferred-restore.js +36 -0
- package/dist/executer/docker-service.js +30 -8
- package/dist/executer/docker-task.js +21 -1
- package/dist/executer/execute-work-service.js +2 -0
- package/dist/executer/execute-work-task.js +11 -4
- package/dist/executer/execution-steps.js +25 -4
- package/dist/executer/reset-work-tree.js +4 -0
- package/dist/executer/scheduler/enqueue-next.js +57 -6
- package/dist/executer/scheduler/state-key.js +42 -5
- package/dist/executer/watch-loop.js +6 -3
- package/dist/harness/connect.js +20 -6
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +63 -114
- package/dist/harness/opencode.js +158 -50
- package/dist/harness/pi.js +29 -0
- package/dist/harness/sandbox-client.js +194 -23
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +221 -0
- package/dist/kubernetes/apply.js +49 -1
- package/dist/kubernetes/await-running-state.js +172 -13
- package/dist/kubernetes/cluster-state.js +245 -0
- package/dist/kubernetes/ensure-kubernetes-deployment-exists.js +133 -9
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/kubernetes/ensure-persistent-data.js +211 -6
- package/dist/kubernetes/follow-pod-logs.js +182 -0
- package/dist/kubernetes/job-placement.js +36 -12
- package/dist/kubernetes/kubernetes-instance.js +1 -0
- package/dist/kubernetes/network-policy.js +2 -1
- package/dist/kubernetes/remove-stale-items.js +46 -0
- package/dist/kubernetes/restore-kubernetes-data.js +9 -5
- package/dist/kubernetes/sandbox-runner.js +628 -0
- package/dist/kubernetes/service-rollouts.js +74 -0
- package/dist/kubernetes/store-kubernetes-data.js +5 -2
- package/dist/kubernetes/volumes.js +94 -9
- package/dist/main.js +2 -0
- package/dist/optimizer/calculate-checksum.js +3 -2
- package/dist/optimizer/check-key.js +94 -0
- package/dist/optimizer/check-keys.js +64 -0
- package/dist/optimizer/get-work-cache-stats.js +17 -3
- package/dist/optimizer/work-task-cache-description.js +49 -1
- package/dist/planner/service-definition.js +9 -4
- package/dist/planner/utils/append-work-dependencies.js +5 -3
- package/dist/planner/utils/append-work-service.js +18 -1
- package/dist/planner/utils/append-work-task.js +13 -3
- package/dist/planner/utils/get-work-runtime.js +6 -1
- package/dist/planner/utils/normalize-path.js +19 -1
- package/dist/planner/utils/parse-work-mount.js +7 -1
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/planner/validate.js +22 -0
- package/dist/planner/work-network.js +3 -1
- package/dist/planner/work-runtime-aggregate.js +32 -0
- package/dist/planner/work-runtime-kubernetes.js +136 -13
- package/dist/planner/work-task.js +6 -1
- package/dist/program.js +27 -9
- package/dist/remote/device-login.js +7 -2
- package/dist/remote/host-config.js +69 -3
- package/dist/remote/remote-commands.js +33 -7
- package/dist/remote/remote-status.js +112 -0
- package/dist/remote/remote-work-commands.js +562 -45
- package/dist/remote/server-client.js +15 -2
- package/dist/remote/token-source.js +64 -26
- package/dist/run-program.js +9 -0
- package/dist/schema/build-file-base-task-schema.js +3 -3
- package/dist/schema/build-file-container-service-schema.js +10 -0
- package/dist/schema/build-file-container-task-schema.js +16 -1
- package/dist/schema/build-file-include-schema.js +18 -4
- package/dist/schema/build-file-kubernetes-service-schema.js +2 -0
- package/dist/schema/build-file-task-schema.js +5 -1
- package/dist/schema/capabilities-schema.js +56 -0
- package/dist/schema/reference-parser.js +119 -11
- package/dist/schema/removed-properties.js +20 -0
- package/dist/schema/schema-parser.js +94 -41
- package/dist/schema/work-scope-parser.js +33 -10
- package/dist/secrets/command-secret-provider.js +3 -1
- package/dist/secrets/github-app-token-command.js +3 -1
- package/dist/secrets/secret-catalog.js +43 -19
- package/dist/server/accounts-api.js +4 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/capacity-format.js +36 -0
- package/dist/server/capacity.js +300 -0
- package/dist/server/checks-api.js +83 -0
- package/dist/server/checks.js +116 -0
- package/dist/server/dashboard.js +196 -0
- package/dist/server/definitions-api.js +66 -3
- package/dist/server/endpoints.js +794 -0
- package/dist/server/executor.js +164 -9
- package/dist/server/kubernetes-backend.js +415 -48
- package/dist/server/kubernetes-sandbox-storage.js +46 -2
- package/dist/server/overview.js +271 -0
- package/dist/server/plan.js +170 -121
- package/dist/server/policy.js +34 -2
- package/dist/server/proposals-api.js +17 -8
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-logs.js +4 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +58 -3
- package/dist/server/run-worker.js +262 -13
- package/dist/server/runs.js +962 -36
- package/dist/server/sandbox-pod.js +28 -0
- package/dist/server/sandbox-storage.js +23 -0
- package/dist/server/sandboxes.js +8 -0
- package/dist/server/server-config.js +246 -10
- package/dist/server/server.js +429 -32
- package/dist/server/service-runs.js +82 -0
- package/dist/server/timings.js +132 -0
- package/dist/server/wait-reason.js +21 -0
- package/dist/utils/tar.js +28 -1
- package/dist/utils/terminal-prompt.js +53 -0
- package/package.json +8 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleDashboard = exports.HEADERS = exports.DECIDE_WITHIN_MS = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const promises_1 = require("fs/promises");
|
|
6
|
+
const path_1 = require("path");
|
|
7
|
+
const http_1 = require("./http");
|
|
8
|
+
const policy_1 = require("./policy");
|
|
9
|
+
const overview_1 = require("./overview");
|
|
10
|
+
// Approving and publishing take a sign-in of the last minutes: the groups of a session are
|
|
11
|
+
// those of its sign-in, and somebody taken out of a group must not decide for hours.
|
|
12
|
+
exports.DECIDE_WITHIN_MS = 15 * 60000;
|
|
13
|
+
const RUN = 'run_[A-Za-z0-9_-]{16}';
|
|
14
|
+
// a hash of a build file version, checked by the API
|
|
15
|
+
const HASH = '[A-Za-z0-9:_-]{1,200}';
|
|
16
|
+
const path = (pattern) => new RegExp(`^/v1/${pattern}$`);
|
|
17
|
+
// what the pages read
|
|
18
|
+
const READS = [
|
|
19
|
+
'me',
|
|
20
|
+
'overview',
|
|
21
|
+
'capacity',
|
|
22
|
+
'tasks',
|
|
23
|
+
'runs',
|
|
24
|
+
`runs/${RUN}`,
|
|
25
|
+
`runs/${RUN}/logs`,
|
|
26
|
+
'approvals',
|
|
27
|
+
`approvals/${RUN}`,
|
|
28
|
+
'proposals',
|
|
29
|
+
`proposals/${HASH}`,
|
|
30
|
+
`proposals/${HASH}/files`,
|
|
31
|
+
].map(path);
|
|
32
|
+
// what the buttons do
|
|
33
|
+
const ACTIONS = [`runs/${RUN}/(cancel|extend)`].map(path);
|
|
34
|
+
// what needs a recent sign-in
|
|
35
|
+
const DECISIONS = [`approvals/${RUN}/(approve|reject)`, `proposals/${HASH}/(approve|reject)`].map(path);
|
|
36
|
+
exports.HEADERS = {
|
|
37
|
+
'cache-control': 'no-store',
|
|
38
|
+
'x-robots-tag': 'noindex',
|
|
39
|
+
// the app is a script and a style sheet of its own host, and talks to its host only
|
|
40
|
+
'content-security-policy': "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; " +
|
|
41
|
+
"font-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'",
|
|
42
|
+
'x-frame-options': 'DENY',
|
|
43
|
+
'referrer-policy': 'no-referrer',
|
|
44
|
+
'x-content-type-options': 'nosniff',
|
|
45
|
+
};
|
|
46
|
+
const TYPES = {
|
|
47
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
48
|
+
'.css': 'text/css; charset=utf-8',
|
|
49
|
+
'.svg': 'image/svg+xml',
|
|
50
|
+
'.woff2': 'font/woff2',
|
|
51
|
+
'.png': 'image/png',
|
|
52
|
+
};
|
|
53
|
+
async function handleDashboard(context, principal, req, res, url) {
|
|
54
|
+
const now = context.now ?? Date.now;
|
|
55
|
+
const grant = (0, policy_1.getGrant)(context.config.entitlements, principal);
|
|
56
|
+
const viewer = { principal, grant, activity: context.config.runs.activity === 'everyone' };
|
|
57
|
+
const pathname = url.pathname;
|
|
58
|
+
for (const [name, value] of Object.entries(exports.HEADERS)) {
|
|
59
|
+
res.setHeader(name, value);
|
|
60
|
+
}
|
|
61
|
+
if (pathname === '/api' || pathname.startsWith('/api/')) {
|
|
62
|
+
if (grant.matched.length === 0) {
|
|
63
|
+
return (0, http_1.send)(res, 403, { error: 'you are not entitled to anything on this server' });
|
|
64
|
+
}
|
|
65
|
+
return api(context, principal, viewer, now(), req, res, url);
|
|
66
|
+
}
|
|
67
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
68
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET, HEAD' });
|
|
69
|
+
}
|
|
70
|
+
return asset(context, req, res, pathname);
|
|
71
|
+
}
|
|
72
|
+
exports.handleDashboard = handleDashboard;
|
|
73
|
+
async function api(context, principal, viewer, now, req, res, url) {
|
|
74
|
+
const { grant } = viewer;
|
|
75
|
+
const csrf = context.sign(`dashboard\0csrf\0${principal.iss}\0${principal.sub}\0${context.session ?? 'token'}`);
|
|
76
|
+
const method = req.method ?? 'GET';
|
|
77
|
+
if (url.pathname === '/api/session') {
|
|
78
|
+
if (method !== 'GET') {
|
|
79
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
80
|
+
}
|
|
81
|
+
const session = {
|
|
82
|
+
iss: principal.iss,
|
|
83
|
+
sub: principal.sub,
|
|
84
|
+
observe: grant.observe,
|
|
85
|
+
seesOthers: grant.observe || (0, overview_1.seesActivity)(viewer),
|
|
86
|
+
publishes: (0, policy_1.mayPublish)(grant),
|
|
87
|
+
csrf,
|
|
88
|
+
...(context.signedInAt !== undefined
|
|
89
|
+
? { decideUntil: new Date(context.signedInAt + exports.DECIDE_WITHIN_MS).toISOString() }
|
|
90
|
+
: {}),
|
|
91
|
+
decideWithinMinutes: exports.DECIDE_WITHIN_MS / 60000,
|
|
92
|
+
};
|
|
93
|
+
return (0, http_1.send)(res, 200, session);
|
|
94
|
+
}
|
|
95
|
+
const download = new RegExp(`^/api/runs/(${RUN})/log\\.txt$`).exec(url.pathname);
|
|
96
|
+
if (download) {
|
|
97
|
+
if (method !== 'GET') {
|
|
98
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'GET' });
|
|
99
|
+
}
|
|
100
|
+
return logText(context, viewer, res, download[1], url.searchParams.get('task') ?? undefined);
|
|
101
|
+
}
|
|
102
|
+
const target = url.pathname.slice('/api'.length);
|
|
103
|
+
const read = READS.some((pattern) => pattern.test(target));
|
|
104
|
+
const action = ACTIONS.some((pattern) => pattern.test(target));
|
|
105
|
+
const decision = DECISIONS.some((pattern) => pattern.test(target));
|
|
106
|
+
if (!read && !action && !decision) {
|
|
107
|
+
return (0, http_1.send)(res, 404, { error: 'not found' });
|
|
108
|
+
}
|
|
109
|
+
if (method === 'GET' || method === 'HEAD') {
|
|
110
|
+
if (!read) {
|
|
111
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (method === 'POST' && (action || decision)) {
|
|
115
|
+
// a page of another site can not set the header, nor read the token
|
|
116
|
+
const given = Buffer.from(String(req.headers['x-hammerkit-csrf'] ?? ''));
|
|
117
|
+
const expected = Buffer.from(csrf);
|
|
118
|
+
if (given.length !== expected.length || !(0, crypto_1.timingSafeEqual)(given, expected)) {
|
|
119
|
+
return (0, http_1.send)(res, 403, { error: 'the page is out of date, open it again' });
|
|
120
|
+
}
|
|
121
|
+
// an API token is new; a sign-in in the browser has to be of the last minutes to decide
|
|
122
|
+
if (decision && context.signedInAt !== undefined && now - context.signedInAt > exports.DECIDE_WITHIN_MS) {
|
|
123
|
+
return (0, http_1.send)(res, 403, {
|
|
124
|
+
error: `approving and rejecting take a sign-in of the last ${exports.DECIDE_WITHIN_MS / 60000} minutes`,
|
|
125
|
+
signInAgain: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
context.log(`dashboard ${method} ${target} iss=${principal.iss} sub=${principal.sub}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
return (0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: read ? 'GET' : 'POST' });
|
|
132
|
+
}
|
|
133
|
+
const forwarded = new URL(`${target}${url.search}`, 'http://localhost');
|
|
134
|
+
return context.api(principal, req, res, forwarded);
|
|
135
|
+
}
|
|
136
|
+
// The log as a text file: what the tasks printed, as they printed it.
|
|
137
|
+
function logText(context, viewer, res, id, task) {
|
|
138
|
+
const run = context.registry.peek(id);
|
|
139
|
+
// the log is for the owner of the run, who decides about it and observe
|
|
140
|
+
const logs = run && (0, overview_1.maySee)(viewer, run) && (0, overview_1.mayRead)(viewer, run) ? context.registry.logsOf(id) : undefined;
|
|
141
|
+
if (!run || !logs) {
|
|
142
|
+
return (0, http_1.send)(res, 404, { error: 'not found' });
|
|
143
|
+
}
|
|
144
|
+
const names = logs.index.names();
|
|
145
|
+
const chosen = task !== undefined && names.includes(task) ? task : undefined;
|
|
146
|
+
const lines = [];
|
|
147
|
+
const selected = chosen ? logs.index.lines(chosen) : undefined;
|
|
148
|
+
const total = selected ? selected.length : logs.index.size;
|
|
149
|
+
for (let position = 0; position < total; position++) {
|
|
150
|
+
const line = logs.index.text(logs.events, selected ? selected[position] : position);
|
|
151
|
+
lines.push(chosen ? line.text : `[${line.task}] ${line.text}`);
|
|
152
|
+
}
|
|
153
|
+
if (logs.truncated) {
|
|
154
|
+
lines.push('[hammerkit] the log was cut off at runs.maxLogBytes');
|
|
155
|
+
}
|
|
156
|
+
res.writeHead(200, {
|
|
157
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
158
|
+
'content-disposition': `attachment; filename="${run.id}${chosen ? `-${chosen.replace(/[^A-Za-z0-9._-]/g, '_')}` : ''}.log"`,
|
|
159
|
+
});
|
|
160
|
+
res.end(lines.join('\n') + (lines.length > 0 ? '\n' : ''));
|
|
161
|
+
}
|
|
162
|
+
// The files of the app: its scripts and styles by their hashed names, and index.html for
|
|
163
|
+
// every page, which the app draws.
|
|
164
|
+
async function asset(context, req, res, pathname) {
|
|
165
|
+
const root = context.assets ?? (0, path_1.join)(__dirname, '..', 'dashboard');
|
|
166
|
+
const file = /^\/assets\/[A-Za-z0-9_-][A-Za-z0-9._-]{0,200}$/.test(pathname)
|
|
167
|
+
? pathname.slice(1)
|
|
168
|
+
: pathname === '/favicon.svg'
|
|
169
|
+
? 'favicon.svg'
|
|
170
|
+
: pathname.startsWith('/assets/') || pathname.startsWith('/.')
|
|
171
|
+
? undefined
|
|
172
|
+
: 'index.html';
|
|
173
|
+
if (!file) {
|
|
174
|
+
return (0, http_1.send)(res, 404, { error: 'not found' });
|
|
175
|
+
}
|
|
176
|
+
let content;
|
|
177
|
+
try {
|
|
178
|
+
content = await (0, promises_1.readFile)((0, path_1.join)(root, file));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
if (file === 'index.html') {
|
|
182
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
|
|
183
|
+
res.end('The dashboard of this server was not built (npm run build:dashboard).\n');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
return (0, http_1.send)(res, 404, { error: 'not found' });
|
|
187
|
+
}
|
|
188
|
+
const extension = /\.[a-z0-9]+$/.exec(file)?.[0] ?? '';
|
|
189
|
+
res.writeHead(200, {
|
|
190
|
+
'content-type': file === 'index.html' ? 'text/html; charset=utf-8' : TYPES[extension] ?? 'application/octet-stream',
|
|
191
|
+
// the names of the scripts and styles change with what they hold
|
|
192
|
+
...(file.startsWith('assets/') ? { 'cache-control': 'public, max-age=31536000, immutable' } : {}),
|
|
193
|
+
});
|
|
194
|
+
res.end(req.method === 'HEAD' ? undefined : content);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -5,6 +5,7 @@ const zod_1 = require("zod");
|
|
|
5
5
|
const bundle_1 = require("../remote/bundle");
|
|
6
6
|
const definitions_1 = require("./definitions");
|
|
7
7
|
const http_1 = require("./http");
|
|
8
|
+
const run_plan_1 = require("./run-plan");
|
|
8
9
|
const oauth_1 = require("./oauth");
|
|
9
10
|
const policy_1 = require("./policy");
|
|
10
11
|
const proposals_1 = require("./proposals");
|
|
@@ -19,12 +20,25 @@ const MAX_PUBLISH_BYTES = bundle_1.MAX_BUNDLE_BYTES * 2;
|
|
|
19
20
|
* `annotations`, which are not part of the hash
|
|
20
21
|
* GET /v1/definitions the versions, newest first; `?<key>=<value>`
|
|
21
22
|
* keeps the ones with those annotations
|
|
22
|
-
* GET /v1/definitions/<hash> one version
|
|
23
|
+
* GET /v1/definitions/<hash> one version, with `arguments`: per task null when
|
|
24
|
+
* a run of it may pass arguments, else why not
|
|
23
25
|
* GET /v1/definitions/<hash>/files its files
|
|
26
|
+
* POST /v1/definitions/plan what publishing a bundle would do: plans it with this
|
|
27
|
+
* server's policy and stores nothing. For anybody with
|
|
28
|
+
* a `builds` level, `run` too
|
|
24
29
|
* POST /v1/definitions/<hash>/yank pull it back (body: optional {reason})
|
|
25
30
|
* POST /v1/definitions/<hash>/unyank let it go on
|
|
26
31
|
*/
|
|
27
32
|
async function handleDefinitions(config, store, proposals, log, principal, req, res, url) {
|
|
33
|
+
if (url.pathname === '/v1/definitions/plan') {
|
|
34
|
+
if (req.method !== 'POST') {
|
|
35
|
+
(0, http_1.send)(res, 405, { error: 'method not allowed' }, { allow: 'POST' });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await plan(config, store, log, principal, req, res);
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
28
42
|
const match = /^\/v1\/definitions(?:\/([^/]+)(\/files|\/yank|\/unyank)?)?$/.exec(url.pathname);
|
|
29
43
|
if (!match) {
|
|
30
44
|
return false;
|
|
@@ -72,7 +86,7 @@ async function handleDefinitions(config, store, proposals, log, principal, req,
|
|
|
72
86
|
bundle ? (0, http_1.send)(res, 200, bundle) : (0, http_1.send)(res, 404, { error: 'not found' });
|
|
73
87
|
}
|
|
74
88
|
else {
|
|
75
|
-
(0, http_1.send)(res, 200, found);
|
|
89
|
+
(0, http_1.send)(res, 200, found.plan ? { ...found, arguments: (0, run_plan_1.describeArguments)(found.plan, config.runs) } : found);
|
|
76
90
|
}
|
|
77
91
|
return true;
|
|
78
92
|
}
|
|
@@ -122,6 +136,55 @@ async function yank(config, store, log, principal, hash, undo, req, res) {
|
|
|
122
136
|
log(`${undo ? 'unyank' : 'yank'} ${hash} iss=${principal.iss} sub=${principal.sub}${body.data.reason ? ` reason=${(0, oauth_1.quote)(body.data.reason)}` : ''}`);
|
|
123
137
|
(0, http_1.send)(res, 200, changed);
|
|
124
138
|
}
|
|
139
|
+
// Answers what POST /v1/definitions would: the version with its plan, and whether the
|
|
140
|
+
// caller would publish or propose it. A developer checks a build file against the policy
|
|
141
|
+
// before the account that publishes does. Nothing is stored, nothing is logged but the call.
|
|
142
|
+
async function plan(config, store, log, principal, req, res) {
|
|
143
|
+
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
144
|
+
if (grant.builds === null) {
|
|
145
|
+
(0, http_1.send)(res, 403, { error: 'you may not check build files, it needs builds: run, propose or publish' });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const parsed = bundle_1.bundleSchema.safeParse(await (0, http_1.readJson)(req, MAX_PUBLISH_BYTES));
|
|
149
|
+
if (!parsed.success) {
|
|
150
|
+
const issue = parsed.error.issues[0];
|
|
151
|
+
(0, http_1.send)(res, 400, { error: `${issue.path.join('.') || 'request'}: ${issue.message}` });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
// like a proposal: a caller who may not publish does not make the server fetch
|
|
156
|
+
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
157
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'check',
|
|
158
|
+
});
|
|
159
|
+
const missing = (version.accounts ?? []).filter((account) => !(0, policy_1.canUse)(grant, account));
|
|
160
|
+
const outcome = existing
|
|
161
|
+
? 'already published'
|
|
162
|
+
: (0, policy_1.mayPublish)(grant) && missing.length === 0
|
|
163
|
+
? 'published'
|
|
164
|
+
: (0, policy_1.mayPublish)(grant) || (0, policy_1.mayPropose)(grant)
|
|
165
|
+
? 'proposed'
|
|
166
|
+
: null;
|
|
167
|
+
log(`plan ${version.hash} iss=${principal.iss} sub=${principal.sub} tasks=${version.tasks.length}`);
|
|
168
|
+
(0, http_1.send)(res, 200, {
|
|
169
|
+
hash: version.hash,
|
|
170
|
+
tasks: version.tasks,
|
|
171
|
+
services: version.services,
|
|
172
|
+
plan: version.plan,
|
|
173
|
+
accounts: version.accounts,
|
|
174
|
+
outcome,
|
|
175
|
+
// a yanked version is not checked again: it is the one that was published
|
|
176
|
+
...(existing?.yanked ? { yanked: true } : {}),
|
|
177
|
+
...(missing.length > 0 ? { missing } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (e) {
|
|
181
|
+
if (e instanceof definitions_1.DefinitionError) {
|
|
182
|
+
(0, http_1.send)(res, 400, { error: `the build file does not load: ${e.message}` });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
125
188
|
async function publish(config, store, proposals, log, principal, req, res) {
|
|
126
189
|
const grant = (0, policy_1.getGrant)(config.entitlements, principal);
|
|
127
190
|
if (!(0, policy_1.mayPublish)(grant) && !(0, policy_1.mayPropose)(grant)) {
|
|
@@ -149,7 +212,7 @@ async function publish(config, store, proposals, log, principal, req, res) {
|
|
|
149
212
|
// a version can't run before it is approved, but it has to load, as it would
|
|
150
213
|
// have when published, so the approver is not asked about a broken one
|
|
151
214
|
const { version, existing } = await store.prepare(parsed.data, principal, {
|
|
152
|
-
noRemote:
|
|
215
|
+
noRemote: (0, policy_1.mayPublish)(grant) ? undefined : 'proposal',
|
|
153
216
|
annotations,
|
|
154
217
|
});
|
|
155
218
|
if (existing) {
|