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,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.serviceRollouts = exports.ServiceRollouts = void 0;
|
|
4
|
+
const apply_1 = require("./apply");
|
|
5
|
+
const log_1 = require("../log");
|
|
6
|
+
/**
|
|
7
|
+
* The Deployments an execution of a service run replaced or made (rollOut). A move whose
|
|
8
|
+
* services do not all get up leaves the run where it was: the execution puts every one of
|
|
9
|
+
* them back as it found it and removes the ones it made, so no service serves the commit the
|
|
10
|
+
* run did not get to, and nothing of it takes the quota of the namespace.
|
|
11
|
+
*/
|
|
12
|
+
class ServiceRollouts {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.rolled = new Map();
|
|
15
|
+
}
|
|
16
|
+
// before the Deployment is replaced or made; the first time counts
|
|
17
|
+
add(namespace, name, previous, restore) {
|
|
18
|
+
const key = `${namespace}/${name}`;
|
|
19
|
+
if (!this.rolled.has(key)) {
|
|
20
|
+
this.rolled.set(key, {
|
|
21
|
+
namespace,
|
|
22
|
+
name,
|
|
23
|
+
...(previous?.spec ? { previous } : {}),
|
|
24
|
+
...(restore ? { restore } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
get size() {
|
|
29
|
+
return this.rolled.size;
|
|
30
|
+
}
|
|
31
|
+
async undo(instance, say) {
|
|
32
|
+
const rolled = [...this.rolled.values()];
|
|
33
|
+
this.rolled.clear();
|
|
34
|
+
for (const { namespace, name, previous, restore } of rolled) {
|
|
35
|
+
try {
|
|
36
|
+
if (previous?.spec && restore) {
|
|
37
|
+
say(`putting the files of ${name} back as they were`);
|
|
38
|
+
await restore();
|
|
39
|
+
}
|
|
40
|
+
if (previous?.spec) {
|
|
41
|
+
say(`putting ${name} back as it was`);
|
|
42
|
+
await (0, apply_1.replaceDeployment)(instance, {
|
|
43
|
+
kind: 'Deployment',
|
|
44
|
+
apiVersion: 'apps/v1',
|
|
45
|
+
metadata: {
|
|
46
|
+
namespace,
|
|
47
|
+
name,
|
|
48
|
+
labels: previous.metadata?.labels,
|
|
49
|
+
annotations: previous.metadata?.annotations,
|
|
50
|
+
},
|
|
51
|
+
spec: previous.spec,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
say(`removing ${name}, which it made`);
|
|
56
|
+
await instance.appsApi
|
|
57
|
+
.deleteNamespacedDeployment({ name, namespace, propagationPolicy: 'Background' })
|
|
58
|
+
.catch((e) => {
|
|
59
|
+
if ((0, apply_1.statusCodeOf)(e) !== 404) {
|
|
60
|
+
throw e;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
say(`${name} could not be put back: ${(0, log_1.getErrorMessage)(e)}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.ServiceRollouts = ServiceRollouts;
|
|
72
|
+
// the rollouts of this process: a worker executes one run
|
|
73
|
+
exports.serviceRollouts = new ServiceRollouts();
|
|
74
|
+
//# sourceMappingURL=service-rollouts.js.map
|
|
@@ -5,8 +5,11 @@ const log_1 = require("../log");
|
|
|
5
5
|
const event_cache_1 = require("../executer/event-cache");
|
|
6
6
|
const volumes_1 = require("./volumes");
|
|
7
7
|
const ensure_persistent_data_1 = require("./ensure-persistent-data");
|
|
8
|
-
async function storeKubernetesData(work, kubernetes, instance, environment, path
|
|
9
|
-
|
|
8
|
+
async function storeKubernetesData(work, kubernetes, instance, environment, path,
|
|
9
|
+
// the state the outputs are of: on a service run, they are in its folder (see
|
|
10
|
+
// getStateFilesDir)
|
|
11
|
+
stateKey) {
|
|
12
|
+
const persistence = await (0, volumes_1.getKubernetesPersistence)(work, kubernetes, stateKey);
|
|
10
13
|
await (0, ensure_persistent_data_1.getPodForPersistence)(instance, kubernetes, work, persistence, 'read', async (name) => {
|
|
11
14
|
for (const generatedArchive of (0, event_cache_1.getArchivePaths)(work.data, path)) {
|
|
12
15
|
const writerStream = environment.file.createWriteStream(generatedArchive.filename);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getSandboxAffinity = exports.SANDBOX_LABEL = exports.getWorkspacePodSpec = exports.appendVolume = exports.getKubernetesPersistence = exports.getVolumeName = void 0;
|
|
3
|
+
exports.getItemFilesDir = exports.isLiveService = exports.LIVE_FILES_DIR = exports.getStateFilesDir = exports.STATE_FILES_PREFIX = exports.getSandboxAffinity = exports.SANDBOX_LABEL = exports.getClaimMountLabel = exports.getClaimPodSpec = exports.getWorkspacePodSpec = exports.appendVolume = exports.getKubernetesPersistence = exports.getVolumeName = void 0;
|
|
4
4
|
const path_1 = require("path");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
6
7
|
const kept_claims_1 = require("./kept-claims");
|
|
7
8
|
const claim_layout_1 = require("./claim-layout");
|
|
8
9
|
function getVolumeName(service, kubernetes) {
|
|
@@ -41,7 +42,15 @@ function appendWorkspaceMount(workspace, persistence, containerPath, localPath,
|
|
|
41
42
|
});
|
|
42
43
|
return true;
|
|
43
44
|
}
|
|
44
|
-
async function getKubernetesPersistence(work, kubernetes
|
|
45
|
+
async function getKubernetesPersistence(work, kubernetes,
|
|
46
|
+
// the state the work runs at, on a service run (see getStateFilesDir); what it inherits is
|
|
47
|
+
// at the state its own work item completed at
|
|
48
|
+
stateKey) {
|
|
49
|
+
const dirOf = (item) => kubernetes.server?.serviceRun ? getItemFilesDir(item, item === work ? stateKey : undefined) : undefined;
|
|
50
|
+
const own = dirOf(work);
|
|
51
|
+
// a service that watches its files mounts them from the folder they are synced into, and
|
|
52
|
+
// takes them into the folder of their state (see syncLiveFiles)
|
|
53
|
+
const mounted = own && isLiveService(work, kubernetes) ? exports.LIVE_FILES_DIR : own;
|
|
45
54
|
const persistence = {
|
|
46
55
|
volumes: [],
|
|
47
56
|
sizes: {},
|
|
@@ -57,7 +66,7 @@ async function getKubernetesPersistence(work, kubernetes) {
|
|
|
57
66
|
appendWorkspaceMount(workspace, persistence, mount.containerPath, mount.localPath, mount.isFile, mount.readOnly)) {
|
|
58
67
|
continue;
|
|
59
68
|
}
|
|
60
|
-
appendVolume(work, persistence, mount.containerPath, mount.localPath, mount.isFile, true, kubernetes);
|
|
69
|
+
appendVolume(work, persistence, mount.containerPath, mount.localPath, mount.isFile, true, kubernetes, mounted, own);
|
|
61
70
|
}
|
|
62
71
|
for (const src of work.data.src) {
|
|
63
72
|
if (src.inherited) {
|
|
@@ -67,24 +76,32 @@ async function getKubernetesPersistence(work, kubernetes) {
|
|
|
67
76
|
appendWorkspaceMount(workspace, persistence, src.absolutePath, src.absolutePath, src.isFile, false)) {
|
|
68
77
|
continue;
|
|
69
78
|
}
|
|
70
|
-
appendVolume(work, persistence, src.absolutePath, src.absolutePath, src.isFile, true, kubernetes); // TODO matcher
|
|
79
|
+
appendVolume(work, persistence, src.absolutePath, src.absolutePath, src.isFile, true, kubernetes, mounted, own); // TODO matcher
|
|
71
80
|
}
|
|
72
81
|
if (work.data.type === 'container-task') {
|
|
73
82
|
for (const generate of work.data.generates) {
|
|
74
|
-
appendVolume(generate.inherited ?? work, persistence, generate.path, null, false, !!generate.inherited, kubernetes);
|
|
83
|
+
appendVolume(generate.inherited ?? work, persistence, generate.path, null, false, !!generate.inherited, kubernetes, dirOf(generate.inherited ?? work));
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
else {
|
|
78
87
|
for (const volume of work.data.volumes) {
|
|
79
|
-
appendVolume(volume.inherited ?? work, persistence, volume.containerPath, null, false, !!volume.inherited || volume.readOnly, kubernetes
|
|
88
|
+
appendVolume(volume.inherited ?? work, persistence, volume.containerPath, null, false, !!volume.inherited || volume.readOnly, kubernetes,
|
|
89
|
+
// the data of a service stays where it is, the outputs of its deps are by their state
|
|
90
|
+
volume.inherited ? dirOf(volume.inherited) : undefined);
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
return persistence;
|
|
83
94
|
}
|
|
84
95
|
exports.getKubernetesPersistence = getKubernetesPersistence;
|
|
85
|
-
function appendVolume(sourceWork, persistence, containerPath, localPath, isFile, readOnly, kubernetes
|
|
96
|
+
function appendVolume(sourceWork, persistence, containerPath, localPath, isFile, readOnly, kubernetes,
|
|
97
|
+
// the folder of the state of `sourceWork` the path is in, see getStateFilesDir
|
|
98
|
+
stateDir,
|
|
99
|
+
// the folder its upload goes to, when it is not the one the path is mounted from (a
|
|
100
|
+
// service that watches its files, see syncLiveFiles)
|
|
101
|
+
uploadDir = stateDir) {
|
|
86
102
|
const volumeName = getVolumeName(sourceWork, kubernetes);
|
|
87
|
-
const
|
|
103
|
+
const claimPath = (0, claim_layout_1.getClaimSubPath)(sourceWork, containerPath);
|
|
104
|
+
const subPath = (stateDir ? `${stateDir}/` : '') + claimPath;
|
|
88
105
|
const existVolume = persistence.volumes.some((v) => v.name === volumeName);
|
|
89
106
|
if (!existVolume) {
|
|
90
107
|
persistence.volumes.push({
|
|
@@ -113,7 +130,7 @@ function appendVolume(sourceWork, persistence, containerPath, localPath, isFile,
|
|
|
113
130
|
localPath: localPath,
|
|
114
131
|
matcher: () => true, // TODO
|
|
115
132
|
containerPath,
|
|
116
|
-
subPath,
|
|
133
|
+
subPath: (uploadDir ? `${uploadDir}/` : '') + claimPath,
|
|
117
134
|
stateKey: '', // TODO
|
|
118
135
|
});
|
|
119
136
|
}
|
|
@@ -128,6 +145,9 @@ exports.appendVolume = appendVolume;
|
|
|
128
145
|
*/
|
|
129
146
|
function getWorkspacePodSpec(kubernetes, persistence, task) {
|
|
130
147
|
const workspace = kubernetes.server?.workspace;
|
|
148
|
+
if (kubernetes.server?.serviceRun && !workspace) {
|
|
149
|
+
return getClaimPodSpec(persistence.volumes ?? []);
|
|
150
|
+
}
|
|
131
151
|
if (!workspace || persistence.workspace.length === 0) {
|
|
132
152
|
return { labels: {} };
|
|
133
153
|
}
|
|
@@ -141,6 +161,41 @@ function getWorkspacePodSpec(kubernetes, persistence, task) {
|
|
|
141
161
|
};
|
|
142
162
|
}
|
|
143
163
|
exports.getWorkspacePodSpec = getWorkspacePodSpec;
|
|
164
|
+
/**
|
|
165
|
+
* A pod of a service run that mounts claims: labelled by each of them, and made to go where a
|
|
166
|
+
* pod runs that mounts all of them. A run that moves to another commit starts a new pod of a
|
|
167
|
+
* service, a Job of a changed task or an upload while the pods of the commit before still
|
|
168
|
+
* mount the same claims, which a cloud disk attaches to one node at a time: a pod on another
|
|
169
|
+
* node would wait for the disk until its deadline. Required, as one term: while no pod mounts
|
|
170
|
+
* all of them (the first one, or after the run slept), the scheduler lets the pod go anywhere,
|
|
171
|
+
* because it matches the term itself, so a Deployment that keeps the term is never held back
|
|
172
|
+
* by it. Mounting all of them is what holds the disks on one node.
|
|
173
|
+
*/
|
|
174
|
+
function getClaimPodSpec(volumes) {
|
|
175
|
+
const keys = [
|
|
176
|
+
...new Set(volumes.flatMap((volume) => volume.persistentVolumeClaim ? [getClaimMountLabel(volume.persistentVolumeClaim.claimName)] : [])),
|
|
177
|
+
].sort();
|
|
178
|
+
if (keys.length === 0) {
|
|
179
|
+
return { labels: {} };
|
|
180
|
+
}
|
|
181
|
+
const labels = Object.fromEntries(keys.map((key) => [key, 'mounted']));
|
|
182
|
+
return {
|
|
183
|
+
labels,
|
|
184
|
+
affinity: {
|
|
185
|
+
podAffinity: {
|
|
186
|
+
requiredDuringSchedulingIgnoredDuringExecution: [
|
|
187
|
+
{ labelSelector: { matchLabels: labels }, topologyKey: 'kubernetes.io/hostname' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
exports.getClaimPodSpec = getClaimPodSpec;
|
|
194
|
+
// a claim name is longer than a label name may be: its hash
|
|
195
|
+
function getClaimMountLabel(claimName) {
|
|
196
|
+
return `claim.hammerkit.dev/${(0, crypto_1.createHash)('sha256').update(claimName).digest('hex').slice(0, 16)}`;
|
|
197
|
+
}
|
|
198
|
+
exports.getClaimMountLabel = getClaimMountLabel;
|
|
144
199
|
// the label of the pods that mount the claim of a sandbox
|
|
145
200
|
exports.SANDBOX_LABEL = 'hammerkit.dev/sandbox';
|
|
146
201
|
function getSandboxAffinity(label) {
|
|
@@ -153,4 +208,34 @@ function getSandboxAffinity(label) {
|
|
|
153
208
|
};
|
|
154
209
|
}
|
|
155
210
|
exports.getSandboxAffinity = getSandboxAffinity;
|
|
211
|
+
// The folder of a service run's claim that holds the files of one state of a work item: the
|
|
212
|
+
// src and mounts it uploads, and the outputs of a task. A run that moves to another commit
|
|
213
|
+
// uploads and builds the files of the new state into a folder of their own, so the pods of
|
|
214
|
+
// the commit before keep the files and outputs they started with until the new ones are up,
|
|
215
|
+
// and a Deployment that is put back finds them again. The volumes of a service hold its data,
|
|
216
|
+
// which stays where it is.
|
|
217
|
+
exports.STATE_FILES_PREFIX = '.hammerkit-files-';
|
|
218
|
+
function getStateFilesDir(stateKey) {
|
|
219
|
+
return `${exports.STATE_FILES_PREFIX}${(0, crypto_1.createHash)('sha256').update(stateKey).digest('hex').slice(0, 16)}`;
|
|
220
|
+
}
|
|
221
|
+
exports.getStateFilesDir = getStateFilesDir;
|
|
222
|
+
// The folder of the claim of a service of a service run that watches its own files
|
|
223
|
+
// (`continuous`), which its pod mounts its `src` and `mounts` from. The files of each state
|
|
224
|
+
// are uploaded into the folder of that state, and synced into this one from there: the pod
|
|
225
|
+
// stays, and the service sees the files of the new commit (syncLiveFiles).
|
|
226
|
+
exports.LIVE_FILES_DIR = `${exports.STATE_FILES_PREFIX}live`;
|
|
227
|
+
function isLiveService(work, kubernetes) {
|
|
228
|
+
return (!!kubernetes.server?.serviceRun &&
|
|
229
|
+
!kubernetes.server.workspace &&
|
|
230
|
+
work.data.type === 'container-service' &&
|
|
231
|
+
work.data.continuous === true);
|
|
232
|
+
}
|
|
233
|
+
exports.isLiveService = isLiveService;
|
|
234
|
+
// The folder of the state of `item`: `stateKey`, or the one it completed at in this run
|
|
235
|
+
function getItemFilesDir(item, stateKey) {
|
|
236
|
+
const key = stateKey ??
|
|
237
|
+
item.state?.current.stateKey;
|
|
238
|
+
return key ? getStateFilesDir(key) : undefined;
|
|
239
|
+
}
|
|
240
|
+
exports.getItemFilesDir = getItemFilesDir;
|
|
156
241
|
//# sourceMappingURL=volumes.js.map
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,7 @@ const redact_1 = require("./utils/redact");
|
|
|
8
8
|
const empty_writable_1 = require("./utils/empty-writable");
|
|
9
9
|
const run_program_1 = require("./run-program");
|
|
10
10
|
const abort_on_signals_1 = require("./utils/abort-on-signals");
|
|
11
|
+
const terminal_prompt_1 = require("./utils/terminal-prompt");
|
|
11
12
|
const abortCtrl = new AbortController();
|
|
12
13
|
(0, abort_on_signals_1.abortOnSignals)(process, abortCtrl);
|
|
13
14
|
let settled = false;
|
|
@@ -32,6 +33,7 @@ const secrets = (0, redact_1.createSecretRegistry)();
|
|
|
32
33
|
stdout: process.stdout,
|
|
33
34
|
stderr: process.stderr,
|
|
34
35
|
stdoutColumns: process.stdout.columns,
|
|
36
|
+
prompt: (0, terminal_prompt_1.getTerminalPrompt)(process.env, abortCtrl.signal),
|
|
35
37
|
}, process.argv, false).then(() => {
|
|
36
38
|
settled = true;
|
|
37
39
|
}, () => {
|
|
@@ -4,9 +4,10 @@ exports.calculateChecksum = void 0;
|
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
// Hashes the raw bytes: decoding to text first would map every invalid UTF-8
|
|
6
6
|
// sequence to U+FFFD, so two different binary files could share a checksum.
|
|
7
|
-
// For UTF-8 text the result is unchanged.
|
|
7
|
+
// For UTF-8 text the result is unchanged. sha256, which nobody can make two files share:
|
|
8
|
+
// a cache hit is also what a verified check counts (check-key.ts).
|
|
8
9
|
async function calculateChecksum(environment, path) {
|
|
9
|
-
const hash = (0, crypto_1.createHash)('
|
|
10
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
10
11
|
for await (const chunk of environment.file.readStream(path)) {
|
|
11
12
|
hash.update(chunk);
|
|
12
13
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCheckKey = void 0;
|
|
4
|
+
const crypto_1 = require("crypto");
|
|
5
|
+
const state_key_1 = require("../executer/scheduler/state-key");
|
|
6
|
+
const get_work_cache_stats_1 = require("./get-work-cache-stats");
|
|
7
|
+
const work_task_cache_description_1 = require("./work-task-cache-description");
|
|
8
|
+
/**
|
|
9
|
+
* What a passed check is recorded under, and what `remote verify` looks it up by: the
|
|
10
|
+
* inputs of the task's cache key, so a cached result counts for the same key it was
|
|
11
|
+
* stored under. Its definition (the services it needs included), the files of its `src`,
|
|
12
|
+
* the keys of its dependencies, and the sources of the services it needs and of the tasks
|
|
13
|
+
* they depend on.
|
|
14
|
+
*
|
|
15
|
+
* It is computed on a checkout before anything ran in it, on both ends, so it also covers
|
|
16
|
+
* the files a commit holds where a dependency generates (the cache leaves those out, as
|
|
17
|
+
* they change once the dependency ran), and it leaves out `.git`, which is no file of a
|
|
18
|
+
* commit. Two checkouts of the same files give the same key wherever they are.
|
|
19
|
+
* Null for a task that can not be proven up to date (no `src` of its own, or none that
|
|
20
|
+
* matches a file, here or in a dependency): its result says nothing about any files.
|
|
21
|
+
*/
|
|
22
|
+
async function getCheckKey(item, environment, memo = new Map(), path = new Set()) {
|
|
23
|
+
return memoized(memo, path, `task\0${item.id()}`, (inner) => computeCheckKey(item, environment, memo, inner));
|
|
24
|
+
}
|
|
25
|
+
exports.getCheckKey = getCheckKey;
|
|
26
|
+
async function computeCheckKey(item, environment, memo, path) {
|
|
27
|
+
const { stats, unmatched } = await (0, get_work_cache_stats_1.getWorkItemCheckoutStats)(item, environment);
|
|
28
|
+
if (!(0, state_key_1.isOwnProvable)(item, unmatched)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const inputs = await getInputKeys(item, environment, memo, path);
|
|
32
|
+
if (inputs === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return hash({ task: (0, work_task_cache_description_1.getWorkTaskCacheDescription)(item), files: commitFiles(stats), ...inputs });
|
|
36
|
+
}
|
|
37
|
+
// The sources of a needed service, with the keys of what it depends on and needs; its
|
|
38
|
+
// definition is in the description of the task that needs it.
|
|
39
|
+
function getNeedKey(item, environment, memo, path) {
|
|
40
|
+
return memoized(memo, path, `need\0${item.name}`, async (inner) => {
|
|
41
|
+
const inputs = await getInputKeys(item, environment, memo, inner);
|
|
42
|
+
if (inputs === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return hash({ files: commitFiles(await (0, get_work_cache_stats_1.getWorkCacheStats)(item.data, environment)), ...inputs });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function getInputKeys(item, environment, memo, path) {
|
|
49
|
+
const deps = [];
|
|
50
|
+
for (const dep of item.deps) {
|
|
51
|
+
const key = await getCheckKey(dep, environment, memo, path);
|
|
52
|
+
if (key === null) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
deps.push(key);
|
|
56
|
+
}
|
|
57
|
+
const needs = [];
|
|
58
|
+
for (const need of item.needs) {
|
|
59
|
+
const key = await getNeedKey(need.service, environment, memo, path);
|
|
60
|
+
if (key === null) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
needs.push(`${need.name}=${key}`);
|
|
64
|
+
}
|
|
65
|
+
return { deps: deps.sort(), needs: needs.sort() };
|
|
66
|
+
}
|
|
67
|
+
// a commit never holds `.git`: what a checkout keeps there differs between a CI job's clone,
|
|
68
|
+
// the server's fetch and a sandbox's copy, which has none
|
|
69
|
+
function commitFiles(stats) {
|
|
70
|
+
return Object.keys(stats.files)
|
|
71
|
+
.filter((path) => !path.split('/').includes('.git'))
|
|
72
|
+
.sort()
|
|
73
|
+
.map((path) => [path, stats.files[path].checksum]);
|
|
74
|
+
}
|
|
75
|
+
function hash(value) {
|
|
76
|
+
return (0, crypto_1.createHash)('sha256').update(JSON.stringify(value)).digest('hex');
|
|
77
|
+
}
|
|
78
|
+
// `path` holds the keys being computed on the way here: one coming back is a loop (services
|
|
79
|
+
// that need each other), which can not be proven and fails to plan anyway. Waiting on its
|
|
80
|
+
// pending promise would never end. The keys are computed one after another, so every pending
|
|
81
|
+
// promise in the memo is on the path.
|
|
82
|
+
function memoized(memo, path, key, compute) {
|
|
83
|
+
if (path.has(key)) {
|
|
84
|
+
return Promise.resolve(null);
|
|
85
|
+
}
|
|
86
|
+
const known = memo.get(key);
|
|
87
|
+
if (known) {
|
|
88
|
+
return known;
|
|
89
|
+
}
|
|
90
|
+
const computed = compute(new Set([...path, key]));
|
|
91
|
+
memo.set(key, computed);
|
|
92
|
+
return computed;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=check-key.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCheckKeys = void 0;
|
|
4
|
+
const plan_work_tasks_1 = require("../planner/utils/plan-work-tasks");
|
|
5
|
+
const reference_parser_1 = require("../schema/reference-parser");
|
|
6
|
+
const schema_parser_1 = require("../schema/schema-parser");
|
|
7
|
+
const work_scope_parser_1 = require("../schema/work-scope-parser");
|
|
8
|
+
const log_1 = require("../log");
|
|
9
|
+
const check_key_1 = require("./check-key");
|
|
10
|
+
/**
|
|
11
|
+
* The check keys of the tasks of a scope, as both ends compute them: the process of a run on
|
|
12
|
+
* the server before it executes anything, and `remote verify` in a CI job's checkout. So the
|
|
13
|
+
* build file is read the same way on both, as a server reads it (no `.env`, links are files)
|
|
14
|
+
* but placed nowhere: where a server runs a task (a pool, its tool image, the arch of its
|
|
15
|
+
* nodes) is the server's, and a result counts for the server it passed on. The secrets'
|
|
16
|
+
* providers and accounts are the server's too, and no part of a key, so any is taken here.
|
|
17
|
+
*/
|
|
18
|
+
async function getCheckKeys(entry, environment, scope) {
|
|
19
|
+
const keyed = { ...environment, hosted: { catalog: anyCatalog(), localTasksInContainers: true } };
|
|
20
|
+
const { ctx, scope: parsed } = await (0, schema_parser_1.createParseContext)(entry, keyed);
|
|
21
|
+
const workTree = (0, work_scope_parser_1.getWorkContext)(await (0, reference_parser_1.parseReferences)(ctx, parsed, keyed), scope, keyed);
|
|
22
|
+
const memo = new Map();
|
|
23
|
+
const keys = {};
|
|
24
|
+
for (const task of (0, plan_work_tasks_1.iterateWorkTasks)(workTree)) {
|
|
25
|
+
try {
|
|
26
|
+
const key = await (0, check_key_1.getCheckKey)(task, keyed, memo);
|
|
27
|
+
keys[task.name] = key
|
|
28
|
+
? { key }
|
|
29
|
+
: {
|
|
30
|
+
key: null,
|
|
31
|
+
reason: 'it, or a task it or a service it needs depends on, declares no src that matches a file',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
// a secret whose value is part of the key (`cache: true`) is not known here
|
|
36
|
+
keys[task.name] = { key: null, reason: (0, log_1.getErrorMessage)(e) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return keys;
|
|
40
|
+
}
|
|
41
|
+
exports.getCheckKeys = getCheckKeys;
|
|
42
|
+
// Every provider and account name resolves, to a definition nothing runs.
|
|
43
|
+
function anyCatalog() {
|
|
44
|
+
const provider = (name) => ({
|
|
45
|
+
name,
|
|
46
|
+
type: 'command',
|
|
47
|
+
command: [],
|
|
48
|
+
env: {},
|
|
49
|
+
timeoutMs: 0,
|
|
50
|
+
declaredIn: 'the server',
|
|
51
|
+
});
|
|
52
|
+
const account = (name) => ({
|
|
53
|
+
name,
|
|
54
|
+
default: false,
|
|
55
|
+
providers: new Proxy({}, { get: () => ({ env: {} }) }),
|
|
56
|
+
declaredIn: 'the server',
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
providers: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? provider(name) : undefined) }),
|
|
60
|
+
accounts: new Proxy({}, { get: (_target, name) => (typeof name === 'string' ? account(name) : undefined) }),
|
|
61
|
+
explicitAccounts: true,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=check-keys.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getStateKey = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
|
|
3
|
+
exports.stateKeyDigest = exports.getStateKey = exports.getWorkItemCheckoutStats = exports.getWorkItemCacheStats = exports.getWorkCacheStats = void 0;
|
|
4
4
|
const calculate_checksum_1 = require("./calculate-checksum");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
@@ -20,7 +20,7 @@ async function addWorkCacheStats(result, cwd, path, matcher, context, root, excl
|
|
|
20
20
|
if (!matcher(path)) {
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
|
-
const checksum = (0, crypto_1.createHash)('
|
|
23
|
+
const checksum = (0, crypto_1.createHash)('sha256')
|
|
24
24
|
.update(`link:${await (0, promises_1.readlink)(path)}`)
|
|
25
25
|
.digest('hex');
|
|
26
26
|
result.files[(0, path_1.relative)(cwd, path).split(path_1.sep).join(path_1.posix.sep)] = { lastModified: link.mtimeMs, checksum };
|
|
@@ -85,6 +85,13 @@ function getWorkItemCacheStats(item, environment) {
|
|
|
85
85
|
return collectSourceStats(item.data, environment, dependencyOutputs(item));
|
|
86
86
|
}
|
|
87
87
|
exports.getWorkItemCacheStats = getWorkItemCacheStats;
|
|
88
|
+
// The source stats of a task with what its dependencies generate: on a checkout nothing ran
|
|
89
|
+
// in, those are the files a commit holds there, see check-key.ts.
|
|
90
|
+
async function getWorkItemCheckoutStats(item, environment) {
|
|
91
|
+
const { stats } = await collectSourceStats(item.data, environment, []);
|
|
92
|
+
return { stats, unmatched: (await getWorkItemCacheStats(item, environment)).unmatched };
|
|
93
|
+
}
|
|
94
|
+
exports.getWorkItemCheckoutStats = getWorkItemCheckoutStats;
|
|
88
95
|
function dependencyOutputs(item) {
|
|
89
96
|
const outputs = new Set();
|
|
90
97
|
const visited = new Set();
|
|
@@ -113,7 +120,14 @@ function getStateKey(stats, cacheMethod) {
|
|
|
113
120
|
contents.push(`${key}:${stats.files[key].lastModified}`);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
|
-
return (
|
|
123
|
+
return stateKeyDigest(contents.join(','));
|
|
117
124
|
}
|
|
118
125
|
exports.getStateKey = getStateKey;
|
|
126
|
+
// A state key is also the value of the `hammerkit.dev/state` label on Kubernetes, which
|
|
127
|
+
// takes at most 63 characters: the sha256 in base 36 has 50.
|
|
128
|
+
function stateKeyDigest(content) {
|
|
129
|
+
const hex = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
130
|
+
return BigInt(`0x${hex}`).toString(36).padStart(50, '0');
|
|
131
|
+
}
|
|
132
|
+
exports.stateKeyDigest = stateKeyDigest;
|
|
119
133
|
//# sourceMappingURL=get-work-cache-stats.js.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.storedCacheDescription = exports.getWorkTaskCacheDescription = void 0;
|
|
3
|
+
exports.storedCacheDescription = exports.getWorkServiceCacheIdentity = exports.getWorkTaskCacheDescription = void 0;
|
|
4
4
|
const os_1 = require("os");
|
|
5
5
|
const crypto_1 = require("crypto");
|
|
6
6
|
const replace_env_variables_1 = require("../environment/replace-env-variables");
|
|
7
7
|
const portable_path_1 = require("../planner/utils/portable-path");
|
|
8
8
|
const work_secret_1 = require("../planner/work-secret");
|
|
9
9
|
const claim_layout_1 = require("../kubernetes/claim-layout");
|
|
10
|
+
const work_service_1 = require("../planner/work-service");
|
|
11
|
+
const work_service_id_1 = require("../planner/work-service-id");
|
|
12
|
+
const work_cache_id_1 = require("../planner/work-cache-id");
|
|
10
13
|
// Every path is made project-relative (portablePath) so the description, and the
|
|
11
14
|
// task id hashed from it, is identical wherever the project is checked out.
|
|
12
15
|
//
|
|
@@ -42,9 +45,54 @@ function getWorkTaskCacheDescription(item) {
|
|
|
42
45
|
deps: item.deps.length > 0 ? item.deps.map((dep) => dep.id()).sort() : undefined,
|
|
43
46
|
secrets: cachedSecrets.length > 0 ? cachedSecrets : undefined,
|
|
44
47
|
claimLayout: (0, claim_layout_1.hasPathsEndingAlike)(item) || (0, claim_layout_1.hasOutputsFlatteningAlike)(item) ? 2 : undefined,
|
|
48
|
+
needs: item.needs.length > 0 ? getNeedsIdentities(item) : undefined,
|
|
45
49
|
};
|
|
46
50
|
}
|
|
47
51
|
exports.getWorkTaskCacheDescription = getWorkTaskCacheDescription;
|
|
52
|
+
// `visiting` holds the services on the way here: services that need each other in a loop
|
|
53
|
+
// fail to plan (checkForLoop), the identity only has to come back to say so
|
|
54
|
+
function getNeedsIdentities(item, visiting = new Set()) {
|
|
55
|
+
return (item.needs
|
|
56
|
+
// an init task needs the service it initializes, whose identity holds the init's id
|
|
57
|
+
.filter((need) => !isInitOf(item, need.service))
|
|
58
|
+
.map((need) => visiting.has(need.service)
|
|
59
|
+
? `${need.name}=loop`
|
|
60
|
+
: `${need.name}=${getWorkServiceCacheIdentity(need.service, visiting)}`)
|
|
61
|
+
.sort());
|
|
62
|
+
}
|
|
63
|
+
function isInitOf(item, service) {
|
|
64
|
+
return (0, work_service_1.isContainerWorkService)(service.data) && service.data.init?.task.data === item.data;
|
|
65
|
+
}
|
|
66
|
+
// Everything that makes a service what a task needing it talks to: its definition, the ids of
|
|
67
|
+
// the tasks it depends on and of its init task, and the services it needs in turn. The source
|
|
68
|
+
// of those tasks and of the service is in the state key (computeStateKey), like a dependency's.
|
|
69
|
+
// Not the service id, which leaves out what a service runs (getWorkServiceId).
|
|
70
|
+
function getWorkServiceCacheIdentity(item, visiting = new Set()) {
|
|
71
|
+
const service = item.data;
|
|
72
|
+
const definition = (0, work_service_1.isContainerWorkService)(service)
|
|
73
|
+
? {
|
|
74
|
+
cmd: service.cmd?.cmd ?? null,
|
|
75
|
+
envs: (0, replace_env_variables_1.getEnvironmentVariables)(service.envs),
|
|
76
|
+
healthcheck: service.healthcheck?.cmd.cmd ?? null,
|
|
77
|
+
secrets: service.secrets
|
|
78
|
+
.filter((secret) => secret.cache)
|
|
79
|
+
.map((secret) => `${(0, work_secret_1.getPortableSecretName)(service.projectRoot, secret)}=${secret.digest()}`)
|
|
80
|
+
.sort(),
|
|
81
|
+
// as declared: arguments after -- of a run of the init leave the service as it is
|
|
82
|
+
init: service.init ? (0, work_cache_id_1.getDeclaredWorkTaskId)(service.init.task) : null,
|
|
83
|
+
}
|
|
84
|
+
: { namespace: service.namespace };
|
|
85
|
+
return (0, crypto_1.createHash)('sha256')
|
|
86
|
+
.update(JSON.stringify({
|
|
87
|
+
id: (0, work_service_id_1.getWorkServiceId)(service),
|
|
88
|
+
...definition,
|
|
89
|
+
ports: service.ports.map((port) => `${port.hostPort ?? ''}:${port.containerPort}`).sort(),
|
|
90
|
+
deps: item.deps.map((dep) => dep.id()).sort(),
|
|
91
|
+
needs: getNeedsIdentities(item, new Set([...visiting, item])),
|
|
92
|
+
}))
|
|
93
|
+
.digest('hex');
|
|
94
|
+
}
|
|
95
|
+
exports.getWorkServiceCacheIdentity = getWorkServiceCacheIdentity;
|
|
48
96
|
// The form written to disk: description.json in a cache entry (pushed to remote
|
|
49
97
|
// backends, readable by everyone who can pull) and the local explain record.
|
|
50
98
|
// Env is where tokens get passed, so each value is stored as a digest; the task
|
|
@@ -6,6 +6,7 @@ const work_secret_1 = require("./work-secret");
|
|
|
6
6
|
const work_service_1 = require("./work-service");
|
|
7
7
|
const work_network_1 = require("./work-network");
|
|
8
8
|
const work_cache_id_1 = require("./work-cache-id");
|
|
9
|
+
const portable_path_1 = require("./utils/portable-path");
|
|
9
10
|
// Everything a running container of the service was created from, plus the
|
|
10
11
|
// definitions of the services it needs: a dependent links to the container it
|
|
11
12
|
// started with, so a recreated need means a recreated dependent too. Stored as
|
|
@@ -14,19 +15,23 @@ const work_cache_id_1 = require("./work-cache-id");
|
|
|
14
15
|
function getServiceDefinitionHash(item) {
|
|
15
16
|
const service = item.data;
|
|
16
17
|
const network = (0, work_network_1.getEffectiveNetwork)(service);
|
|
18
|
+
const portable = (path) => (0, portable_path_1.portablePath)(service.projectRoot, path);
|
|
17
19
|
const definition = {
|
|
18
20
|
image: service.image,
|
|
19
21
|
cmd: service.cmd?.cmd ?? null,
|
|
20
22
|
shell: service.shell,
|
|
21
|
-
|
|
23
|
+
// where the checkout is does not count: a service run that moves to another commit runs
|
|
24
|
+
// from another directory beside the first, and keeps what did not change
|
|
25
|
+
workdir: service.workdir ? portable(service.workdir) : null,
|
|
22
26
|
envs: {
|
|
23
27
|
variables: service.envs.variables,
|
|
24
28
|
replacements: service.envs.replacements.map((r) => [r.key, r.name, r.value]),
|
|
25
29
|
},
|
|
26
30
|
ports: service.ports.map((p) => `${p.hostPort ?? ''}:${p.containerPort}`),
|
|
27
|
-
mounts: service.mounts.map((m) => `${m.localPath}:${m.containerPath}:${m.readOnly ? 'ro' : 'rw'}`),
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
mounts: service.mounts.map((m) => `${portable(m.localPath)}:${m.containerPath}:${m.readOnly ? 'ro' : 'rw'}`),
|
|
32
|
+
// the volume of a dep's output is named by where it is, which its path says already
|
|
33
|
+
volumes: service.volumes.map((v) => `${v.inherited ? '' : v.name}:${portable(v.containerPath)}:${v.readOnly ? 'ro' : 'rw'}`),
|
|
34
|
+
src: service.src.map((s) => portable(s.absolutePath)),
|
|
30
35
|
healthcheck: service.healthcheck?.cmd.cmd ?? null,
|
|
31
36
|
// where secrets go, never their values: a rotated value needs a `down`
|
|
32
37
|
secrets: service.secrets.map((s) => s.source.type === 'provider'
|
|
@@ -17,8 +17,10 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// an aggregate hands on what its dependencies generate, as if what depends on it
|
|
21
|
+
// depended on them
|
|
22
|
+
if (((0, work_task_1.isContainerWorkTask)(depNode.data) || (0, work_service_1.isContainerWorkService)(depNode.data) || (0, work_task_1.isAggregateTask)(depNode.data)) &&
|
|
23
|
+
((0, work_task_1.isContainerWorkTask)(item.data) || (0, work_service_1.isContainerWorkService)(item.data) || (0, work_task_1.isAggregateTask)(item.data))) {
|
|
22
24
|
// the outputs of the dep's own deps too, which stay those of the task that made them
|
|
23
25
|
// (on Kubernetes, they are in its claim)
|
|
24
26
|
for (const generate of depNode.data.generates) {
|
|
@@ -39,7 +41,7 @@ function appendWorkDependencies(workTree, referenced, item, environment, context
|
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
else {
|
|
44
|
+
else if ((0, work_task_1.isWorkTask)(item.data)) {
|
|
43
45
|
if (!item.data.generates.some((v) => v.volumeName === generate.volumeName)) {
|
|
44
46
|
item.data.generates.push({
|
|
45
47
|
volumeName: generate.volumeName,
|