@alchemy.run/docker 2.0.0-beta.84 → 2.0.0-beta.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +16 -0
  2. package/lib/Bootstrap.d.ts.map +1 -1
  3. package/lib/Bootstrap.js +2 -1
  4. package/lib/Bootstrap.js.map +1 -1
  5. package/lib/BuildCache.d.ts +87 -0
  6. package/lib/BuildCache.d.ts.map +1 -0
  7. package/lib/BuildCache.js +39 -0
  8. package/lib/BuildCache.js.map +1 -0
  9. package/lib/BuildHash.d.ts +20 -3
  10. package/lib/BuildHash.d.ts.map +1 -1
  11. package/lib/BuildHash.js +60 -21
  12. package/lib/BuildHash.js.map +1 -1
  13. package/lib/BuildPlan.d.ts +202 -0
  14. package/lib/BuildPlan.d.ts.map +1 -0
  15. package/lib/BuildPlan.js +514 -0
  16. package/lib/BuildPlan.js.map +1 -0
  17. package/lib/Container.d.ts +122 -1
  18. package/lib/Container.d.ts.map +1 -1
  19. package/lib/Container.js +232 -4
  20. package/lib/Container.js.map +1 -1
  21. package/lib/Context.d.ts +20 -1
  22. package/lib/Context.d.ts.map +1 -1
  23. package/lib/Context.js +15 -0
  24. package/lib/Context.js.map +1 -1
  25. package/lib/Docker.d.ts +113 -1
  26. package/lib/Docker.d.ts.map +1 -1
  27. package/lib/Docker.js +50 -8
  28. package/lib/Docker.js.map +1 -1
  29. package/lib/Dockerfile.d.ts +0 -2
  30. package/lib/Dockerfile.d.ts.map +1 -1
  31. package/lib/Dockerfile.js.map +1 -1
  32. package/lib/DockerfileParser.d.ts +125 -0
  33. package/lib/DockerfileParser.d.ts.map +1 -0
  34. package/lib/DockerfileParser.js +1040 -0
  35. package/lib/DockerfileParser.js.map +1 -0
  36. package/lib/HealthcheckCommand.d.ts.map +1 -1
  37. package/lib/HealthcheckCommand.js.map +1 -1
  38. package/lib/Image.d.ts +69 -13
  39. package/lib/Image.d.ts.map +1 -1
  40. package/lib/Image.js +59 -9
  41. package/lib/Image.js.map +1 -1
  42. package/lib/ImageArchive.d.ts +41 -0
  43. package/lib/ImageArchive.d.ts.map +1 -0
  44. package/lib/ImageArchive.js +54 -0
  45. package/lib/ImageArchive.js.map +1 -0
  46. package/lib/ImageSource.d.ts +151 -8
  47. package/lib/ImageSource.d.ts.map +1 -1
  48. package/lib/ImageSource.js +247 -72
  49. package/lib/ImageSource.js.map +1 -1
  50. package/lib/Network.d.ts +175 -3
  51. package/lib/Network.d.ts.map +1 -1
  52. package/lib/Network.js +296 -8
  53. package/lib/Network.js.map +1 -1
  54. package/lib/Providers.d.ts.map +1 -1
  55. package/lib/Providers.js.map +1 -1
  56. package/lib/Registry.d.ts.map +1 -1
  57. package/lib/Registry.js.map +1 -1
  58. package/lib/RegistryError.d.ts.map +1 -1
  59. package/lib/RegistryError.js.map +1 -1
  60. package/lib/RemoteImage.d.ts.map +1 -1
  61. package/lib/RemoteImage.js +1 -1
  62. package/lib/RemoteImage.js.map +1 -1
  63. package/lib/Service.d.ts +24 -3
  64. package/lib/Service.d.ts.map +1 -1
  65. package/lib/Service.js +154 -12
  66. package/lib/Service.js.map +1 -1
  67. package/lib/ServiceImage.d.ts.map +1 -1
  68. package/lib/ServiceImage.js.map +1 -1
  69. package/lib/ShellWord.d.ts +28 -0
  70. package/lib/ShellWord.d.ts.map +1 -0
  71. package/lib/ShellWord.js +301 -0
  72. package/lib/ShellWord.js.map +1 -0
  73. package/lib/Swarm.d.ts.map +1 -1
  74. package/lib/Swarm.js.map +1 -1
  75. package/lib/Volume.d.ts +24 -1
  76. package/lib/Volume.d.ts.map +1 -1
  77. package/lib/Volume.js +16 -0
  78. package/lib/Volume.js.map +1 -1
  79. package/lib/index.d.ts.map +1 -1
  80. package/lib/index.js.map +1 -1
  81. package/package.json +9 -9
  82. package/src/Bootstrap.ts +2 -0
  83. package/src/BuildCache.ts +111 -0
  84. package/src/BuildHash.ts +85 -26
  85. package/src/BuildPlan.ts +758 -0
  86. package/src/Container.ts +361 -5
  87. package/src/Context.ts +27 -1
  88. package/src/Docker.ts +157 -9
  89. package/src/Dockerfile.ts +0 -2
  90. package/src/DockerfileParser.ts +1152 -0
  91. package/src/Image.ts +75 -15
  92. package/src/ImageArchive.ts +77 -0
  93. package/src/ImageSource.ts +391 -99
  94. package/src/Network.ts +432 -10
  95. package/src/RemoteImage.ts +1 -1
  96. package/src/Service.ts +246 -62
  97. package/src/ShellWord.ts +326 -0
  98. package/src/Volume.ts +33 -1
package/src/BuildHash.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import * as crypto from "node:crypto";
10
+ import { dotAlchemyDirectory } from "alchemy/AlchemyContext";
10
11
  import {
11
12
  type IgnoreRules,
12
13
  listFileSystemDirectory,
@@ -14,12 +15,18 @@ import {
14
15
  parseIgnoreRules,
15
16
  walkIgnoring,
16
17
  } from "alchemy/Util/Ignore";
18
+ import { initialCwd } from "alchemy/Util/Node";
19
+ import * as Data from "effect/Data";
17
20
  import * as Effect from "effect/Effect";
18
21
  import * as FileSystem from "effect/FileSystem";
19
22
  import * as Path from "effect/Path";
20
- import * as Result from "effect/Result";
21
23
  import * as Stream from "effect/Stream";
22
24
 
25
+ export class BuildHashInvalidProps extends Data.TaggedError("BuildHashInvalidProps")<{
26
+ readonly message: string;
27
+ readonly cause?: unknown;
28
+ }> {}
29
+
23
30
  export interface DockerBuildSource {
24
31
  context: string;
25
32
  dockerfile: string;
@@ -51,10 +58,14 @@ export const resolveDockerBuildPaths = Effect.fn(function* (
51
58
  : path.resolve(context, source.dockerfile);
52
59
 
53
60
  if (!(yield* fs.exists(context))) {
54
- return yield* Effect.fail(new Error(`Docker build context does not exist: ${context}`));
61
+ return yield* new BuildHashInvalidProps({
62
+ message: `Docker build context does not exist: ${context}`,
63
+ });
55
64
  }
56
65
  if (!(yield* fs.exists(dockerfile))) {
57
- return yield* Effect.fail(new Error(`Dockerfile does not exist: ${dockerfile}`));
66
+ return yield* new BuildHashInvalidProps({
67
+ message: `Dockerfile does not exist: ${dockerfile}`,
68
+ });
58
69
  }
59
70
 
60
71
  return { context, dockerfile };
@@ -139,6 +150,25 @@ export const selectDockerBuildContext = Effect.fn(function* (
139
150
  } satisfies DockerBuildContextSelection;
140
151
  });
141
152
 
153
+ /**
154
+ * The context-relative path of Alchemy's `.alchemy` directory when it lies
155
+ * strictly inside `context`, otherwise `undefined`.
156
+ */
157
+ const containedRuntimeDirectory = Effect.fn(function* (context: string) {
158
+ const path = yield* Path.Path;
159
+ const runtime = path.resolve(initialCwd, yield* dotAlchemyDirectory);
160
+ const relative = normalizeRelativePath(path.relative(context, runtime));
161
+ return relative === "" ||
162
+ relative === ".." ||
163
+ relative.startsWith("../") ||
164
+ path.isAbsolute(relative)
165
+ ? undefined
166
+ : relative;
167
+ });
168
+
169
+ /** Files below this size are read in one call instead of streamed. */
170
+ const SMALL_FILE_BYTES = 1024 * 1024;
171
+
142
172
  /**
143
173
  * Hash a Docker build context together with the Dockerfile, platform, and
144
174
  * build arguments. In `effective` mode, the selected `.dockerignore` is
@@ -148,6 +178,15 @@ export const selectDockerBuildContext = Effect.fn(function* (
148
178
  export const hashDockerBuildInputs = Effect.fn(function* (
149
179
  source: DockerBuildSource,
150
180
  mode: DockerBuildHashMode,
181
+ options: {
182
+ /**
183
+ * Directory names left out of the hash at any depth, on top of
184
+ * `.dockerignore` and `.alchemy` — e.g. `.git` and `node_modules` for a
185
+ * dev watcher that must not restart on git's index writes or re-hash
186
+ * installed dependencies on every edit.
187
+ */
188
+ readonly excludeNames?: ReadonlyArray<string>;
189
+ } = {},
151
190
  ) {
152
191
  const fs = yield* FileSystem.FileSystem;
153
192
  const path = yield* Path.Path;
@@ -187,15 +226,35 @@ export const hashDockerBuildInputs = Effect.fn(function* (
187
226
  (entry): entry is string =>
188
227
  entry !== undefined && entry !== ".." && !entry.startsWith("../") && !path.isAbsolute(entry),
189
228
  );
229
+ // Alchemy writes run logs and local state to `.alchemy` on every run. When a
230
+ // context contains it (e.g. `context: "."` at the project root), hashing it
231
+ // would give the image a new tag on every deploy. A context inside
232
+ // `.alchemy` (a generated one) is still hashed in full.
233
+ const runtimeDirectory = yield* containedRuntimeDirectory(context);
234
+ const excludedNames = new Set(options.excludeNames ?? []);
235
+ const listDirectory = yield* listFileSystemDirectory(context);
190
236
  // Walk one level at a time so an excluded directory is discarded before
191
237
  // its contents are read; entries come back sorted, preserving hash order.
192
238
  const entries = yield* walkIgnoring({
193
- list: yield* listFileSystemDirectory(context),
239
+ list:
240
+ runtimeDirectory === undefined && excludedNames.size === 0
241
+ ? listDirectory
242
+ : (directory: string) =>
243
+ listDirectory(directory).pipe(
244
+ Effect.map((children) =>
245
+ children.filter(
246
+ (child) =>
247
+ !(child.type === "Directory" && excludedNames.has(child.name)) &&
248
+ (directory.length === 0 ? child.name : `${directory}/${child.name}`) !==
249
+ runtimeDirectory,
250
+ ),
251
+ ),
252
+ ),
194
253
  rules: dockerignore?.rules,
195
254
  force: forcedPaths,
196
255
  });
197
256
 
198
- for (const { path: entry } of entries) {
257
+ for (const { path: entry, type } of entries) {
199
258
  // The ignore file and Dockerfile contents are hashed above; excluded
200
259
  // entries were only walked so re-included descendants could be found.
201
260
  if (
@@ -206,20 +265,14 @@ export const hashDockerBuildInputs = Effect.fn(function* (
206
265
  }
207
266
 
208
267
  const fullPath = path.join(context, entry);
209
- const hashedEntry = entry;
210
- // FileSystem.stat follows symbolic links, so probe the link itself first.
211
- // Docker preserves the link in the build context; hashing the target file
212
- // would miss retargets between files with identical contents and metadata.
213
- const link = yield* Effect.result(fs.readLink(fullPath));
214
- if (Result.isSuccess(link)) {
268
+ // Docker preserves a symbolic link in the build context; hashing the
269
+ // target file would miss retargets between files with identical
270
+ // contents and metadata. The walk reports the entry's own type, so only
271
+ // links are read as links.
272
+ if (type === "SymbolicLink") {
273
+ const target = yield* fs.readLink(fullPath);
215
274
  yield* Effect.sync(() =>
216
- hasher.update(
217
- `${JSON.stringify({
218
- path: hashedEntry,
219
- type: "SymbolicLink",
220
- target: link.success,
221
- })}\0`,
222
- ),
275
+ hasher.update(`${JSON.stringify({ path: entry, type: "SymbolicLink", target })}\0`),
223
276
  );
224
277
  continue;
225
278
  }
@@ -232,7 +285,7 @@ export const hashDockerBuildInputs = Effect.fn(function* (
232
285
  yield* Effect.sync(() =>
233
286
  hasher.update(
234
287
  `${JSON.stringify({
235
- path: hashedEntry,
288
+ path: entry,
236
289
  type: info.type,
237
290
  mode: info.mode & 0o7777,
238
291
  size: info.type === "File" ? String(info.size) : undefined,
@@ -241,13 +294,19 @@ export const hashDockerBuildInputs = Effect.fn(function* (
241
294
  );
242
295
 
243
296
  if (info.type === "File") {
244
- yield* fs.stream(fullPath).pipe(
245
- Stream.runForEach((chunk) =>
246
- Effect.sync(() => {
247
- hasher.update(chunk);
248
- }),
249
- ),
250
- );
297
+ // Read small files whole (one call); stream large ones so memory stays flat.
298
+ if (info.size < SMALL_FILE_BYTES) {
299
+ const bytes = yield* fs.readFile(fullPath);
300
+ yield* Effect.sync(() => hasher.update(bytes));
301
+ } else {
302
+ yield* fs.stream(fullPath).pipe(
303
+ Stream.runForEach((chunk) =>
304
+ Effect.sync(() => {
305
+ hasher.update(chunk);
306
+ }),
307
+ ),
308
+ );
309
+ }
251
310
  }
252
311
  }
253
312