@alchemy.run/docker 0.0.0-stage → 2.0.0-beta.84

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/LICENSE +201 -0
  2. package/README.md +50 -2
  3. package/lib/Bootstrap.d.ts +7 -0
  4. package/lib/Bootstrap.d.ts.map +1 -0
  5. package/lib/Bootstrap.js +25 -0
  6. package/lib/Bootstrap.js.map +1 -0
  7. package/lib/BuildHash.d.ts +47 -0
  8. package/lib/BuildHash.d.ts.map +1 -0
  9. package/lib/BuildHash.js +165 -0
  10. package/lib/BuildHash.js.map +1 -0
  11. package/lib/Container.d.ts +352 -0
  12. package/lib/Container.d.ts.map +1 -0
  13. package/lib/Container.js +531 -0
  14. package/lib/Container.js.map +1 -0
  15. package/lib/Context.d.ts +86 -0
  16. package/lib/Context.d.ts.map +1 -0
  17. package/lib/Context.js +146 -0
  18. package/lib/Context.js.map +1 -0
  19. package/lib/Docker.d.ts +448 -0
  20. package/lib/Docker.d.ts.map +1 -0
  21. package/lib/Docker.js +416 -0
  22. package/lib/Docker.js.map +1 -0
  23. package/lib/Dockerfile.d.ts +72 -0
  24. package/lib/Dockerfile.d.ts.map +1 -0
  25. package/lib/Dockerfile.js +12 -0
  26. package/lib/Dockerfile.js.map +1 -0
  27. package/lib/HealthcheckCommand.d.ts +17 -0
  28. package/lib/HealthcheckCommand.d.ts.map +1 -0
  29. package/lib/HealthcheckCommand.js +33 -0
  30. package/lib/HealthcheckCommand.js.map +1 -0
  31. package/lib/Image.d.ts +151 -0
  32. package/lib/Image.d.ts.map +1 -0
  33. package/lib/Image.js +192 -0
  34. package/lib/Image.js.map +1 -0
  35. package/lib/ImageSource.d.ts +234 -0
  36. package/lib/ImageSource.d.ts.map +1 -0
  37. package/lib/ImageSource.js +469 -0
  38. package/lib/ImageSource.js.map +1 -0
  39. package/lib/Network.d.ts +70 -0
  40. package/lib/Network.d.ts.map +1 -0
  41. package/lib/Network.js +111 -0
  42. package/lib/Network.js.map +1 -0
  43. package/lib/Providers.d.ts +21 -0
  44. package/lib/Providers.d.ts.map +1 -0
  45. package/lib/Providers.js +26 -0
  46. package/lib/Providers.js.map +1 -0
  47. package/lib/Registry.d.ts +47 -0
  48. package/lib/Registry.d.ts.map +1 -0
  49. package/lib/Registry.js +67 -0
  50. package/lib/Registry.js.map +1 -0
  51. package/lib/RegistryError.d.ts +21 -0
  52. package/lib/RegistryError.d.ts.map +1 -0
  53. package/lib/RegistryError.js +28 -0
  54. package/lib/RegistryError.js.map +1 -0
  55. package/lib/RemoteImage.d.ts +118 -0
  56. package/lib/RemoteImage.d.ts.map +1 -0
  57. package/lib/RemoteImage.js +164 -0
  58. package/lib/RemoteImage.js.map +1 -0
  59. package/lib/Service.d.ts +478 -0
  60. package/lib/Service.d.ts.map +1 -0
  61. package/lib/Service.js +588 -0
  62. package/lib/Service.js.map +1 -0
  63. package/lib/ServiceImage.d.ts +78 -0
  64. package/lib/ServiceImage.d.ts.map +1 -0
  65. package/lib/ServiceImage.js +162 -0
  66. package/lib/ServiceImage.js.map +1 -0
  67. package/lib/Swarm.d.ts +119 -0
  68. package/lib/Swarm.d.ts.map +1 -0
  69. package/lib/Swarm.js +148 -0
  70. package/lib/Swarm.js.map +1 -0
  71. package/lib/Volume.d.ts +94 -0
  72. package/lib/Volume.d.ts.map +1 -0
  73. package/lib/Volume.js +133 -0
  74. package/lib/Volume.js.map +1 -0
  75. package/lib/index.d.ts +14 -0
  76. package/lib/index.d.ts.map +1 -0
  77. package/lib/index.js +13 -0
  78. package/lib/index.js.map +1 -0
  79. package/package.json +72 -3
  80. package/src/Bootstrap.ts +51 -0
  81. package/src/BuildHash.ts +255 -0
  82. package/src/Container.ts +792 -0
  83. package/src/Context.ts +215 -0
  84. package/src/Docker.ts +1079 -0
  85. package/src/Dockerfile.ts +81 -0
  86. package/src/HealthcheckCommand.ts +35 -0
  87. package/src/Image.ts +293 -0
  88. package/src/ImageSource.ts +769 -0
  89. package/src/Network.ts +169 -0
  90. package/src/Providers.ts +46 -0
  91. package/src/Registry.ts +91 -0
  92. package/src/RegistryError.ts +48 -0
  93. package/src/RemoteImage.ts +255 -0
  94. package/src/Service.ts +1080 -0
  95. package/src/ServiceImage.ts +252 -0
  96. package/src/Swarm.ts +221 -0
  97. package/src/Volume.ts +193 -0
  98. package/src/index.ts +17 -0
package/lib/Volume.js ADDED
@@ -0,0 +1,133 @@
1
+ import { Unowned } from "alchemy/AdoptPolicy";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import * as Provider from "alchemy/Provider";
4
+ import { Resource } from "alchemy/Resource";
5
+ import { createInternalTags, hasAlchemyTags, stripInternalTags } from "alchemy/Tags";
6
+ import * as Effect from "effect/Effect";
7
+ import * as Equal from "effect/Equal";
8
+ import { Docker, dockerContextName, dockerPhysicalName } from "./Docker.js";
9
+ /**
10
+ * A Docker volume managed through the active Docker context.
11
+ *
12
+ * Pre-existing same-name volumes are treated as foreign until the engine is
13
+ * allowed to adopt them with `--adopt` or `adopt(true)`.
14
+ *
15
+ *
16
+ * ### Creating Volumes
17
+ * **Example:** Basic volume
18
+ * ```typescript
19
+ * const data = yield* Docker.Volume("data", {
20
+ * name: "app-data",
21
+ * });
22
+ * ```
23
+ *
24
+ * **Example:** PostgreSQL data volume
25
+ * ```typescript
26
+ * const data = yield* Docker.Volume("postgres-data");
27
+ * ```
28
+ *
29
+ * **Example:** Driver options and labels
30
+ * ```typescript
31
+ * const data = yield* Docker.Volume("db-data", {
32
+ * driver: "local",
33
+ * driverOpts: {
34
+ * type: "nfs",
35
+ * o: "addr=10.0.0.1,rw",
36
+ * device: ":/path/to/dir",
37
+ * },
38
+ * labels: {
39
+ * "com.example.usage": "database",
40
+ * },
41
+ * });
42
+ * ```
43
+ *
44
+ * ### Docker Context
45
+ * **Example:** Create a volume in a named Docker context
46
+ * ```typescript
47
+ * const data = yield* Docker.Volume("data", {
48
+ * name: "app-data",
49
+ * context: "remote-build",
50
+ * });
51
+ * ```
52
+ *
53
+ * @resource
54
+ * @product Volume
55
+ */
56
+ export const Volume = Resource("Docker.Volume");
57
+ export const VolumeProvider = () => Provider.effect(Volume, Effect.gen(function* () {
58
+ const docker = yield* Docker;
59
+ return Volume.Provider.of({
60
+ list: () => Effect.succeed([]),
61
+ read: Effect.fn(function* ({ id, instanceId, olds, output }) {
62
+ const context = dockerContextName(olds?.context);
63
+ const name = yield* dockerPhysicalName(id, olds, instanceId);
64
+ const info = yield* docker.volume
65
+ .inspect(name, context)
66
+ .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.undefined));
67
+ if (!info)
68
+ return undefined;
69
+ const attrs = toVolumeAttributes(info);
70
+ if (output)
71
+ return attrs;
72
+ // Without prior state, only adopt a volume that carries our branding;
73
+ // anything else is foreign and gated behind `--adopt`.
74
+ const owned = yield* hasAlchemyTags(id, info.Labels ?? undefined);
75
+ return owned ? attrs : Unowned(attrs);
76
+ }),
77
+ diff: Effect.fn(function* ({ id, instanceId, output, news, olds }) {
78
+ if (!isResolved(news))
79
+ return undefined;
80
+ if (dockerContextName(olds?.context) !== dockerContextName(news?.context)) {
81
+ return { action: "replace", deleteFirst: true };
82
+ }
83
+ const args = yield* makeVolumeArgs(id, news, instanceId);
84
+ // Auto-generated names are engine-owned: the deployed name stays
85
+ // authoritative even if the generator would name this id differently
86
+ // today. Only an explicit user-provided name can force a replace.
87
+ const desiredName = news?.name ?? output?.name ?? args.name;
88
+ if (output?.name !== desiredName ||
89
+ output?.driver !== args.driver ||
90
+ !Equal.equals(output?.driverOpts ?? {}, args.opt ?? {}) ||
91
+ // Compare only user labels; internal `alchemy::*` branding lives on
92
+ // the observed volume but must not drive replacement.
93
+ !Equal.equals(stripInternalTags(output?.labels), args.label ?? {})) {
94
+ return { action: "replace", deleteFirst: true };
95
+ }
96
+ }),
97
+ reconcile: Effect.fn(function* ({ id, instanceId, news, output }) {
98
+ const context = dockerContextName(news?.context);
99
+ const args = yield* makeVolumeArgs(id, news, instanceId);
100
+ // Prefer the deployed name: regenerating would target a different
101
+ // volume if the generator's output for this id ever drifts.
102
+ const name = news?.name ?? output?.name ?? args.name;
103
+ const internalTags = yield* createInternalTags(id);
104
+ const result = yield* docker.volume.create({
105
+ ...args,
106
+ name,
107
+ label: { ...internalTags, ...args.label },
108
+ context,
109
+ });
110
+ return toVolumeAttributes(yield* docker.volume.inspect(result.stdout, context));
111
+ }),
112
+ delete: Effect.fn(({ olds, output }) => docker.volume
113
+ .remove(output.name, dockerContextName(olds?.context))
114
+ .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.void))),
115
+ });
116
+ }));
117
+ // Every prop is optional, so `Docker.Volume("data")` arrives with no props.
118
+ const makeVolumeArgs = (id, props, instanceId) => dockerPhysicalName(id, props, instanceId).pipe(Effect.map((name) => ({
119
+ name,
120
+ driver: props?.driver ?? "local",
121
+ opt: props?.driverOpts,
122
+ label: props?.labels,
123
+ })));
124
+ export const toVolumeAttributes = (info) => ({
125
+ id: info.Name,
126
+ name: info.Name,
127
+ driver: info.Driver,
128
+ driverOpts: info.Options ?? {},
129
+ labels: info.Labels ?? {},
130
+ mountpoint: info.Mountpoint,
131
+ createdAt: Date.parse(info.CreatedAt) || Date.now(),
132
+ });
133
+ //# sourceMappingURL=Volume.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Volume.js","sourceRoot":"","sources":["../src/Volume.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./Container.ts";
2
+ export * from "./Context.ts";
3
+ export * from "./Docker.ts";
4
+ export * as Dockerfile from "./Dockerfile.ts";
5
+ export * from "./Image.ts";
6
+ export * from "./Network.ts";
7
+ export * from "./Providers.ts";
8
+ export type { ImageRegistry } from "./Registry.ts";
9
+ export { DockerRegistryBlobUnknown, DockerRegistryUnavailable, type DockerImagePublicationError, } from "./RegistryError.ts";
10
+ export * from "./RemoteImage.ts";
11
+ export * from "./Service.ts";
12
+ export * from "./Swarm.ts";
13
+ export * from "./Volume.ts";
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EACL,yBAAyB,EACzB,yBAAyB,EACzB,KAAK,2BAA2B,GACjC,MAAM,oBAAoB,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from "./Container.js";
2
+ export * from "./Context.js";
3
+ export * from "./Docker.js";
4
+ export * as Dockerfile from "./Dockerfile.js";
5
+ export * from "./Image.js";
6
+ export * from "./Network.js";
7
+ export * from "./Providers.js";
8
+ export { DockerRegistryBlobUnknown, DockerRegistryUnavailable, } from "./RegistryError.js";
9
+ export * from "./RemoteImage.js";
10
+ export * from "./Service.js";
11
+ export * from "./Swarm.js";
12
+ export * from "./Volume.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,OAAO,KAAK,UAAU,MAAM,iBAAiB,CAAC;AAC9C,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EACL,yBAAyB,EACzB,yBAAyB,GAE1B,MAAM,oBAAoB,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,75 @@
1
1
  {
2
2
  "name": "@alchemy.run/docker",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "2.0.0-beta.84",
4
+ "description": "Docker images, containers, networks, volumes and registries for Alchemy, and the image builds container platforms share.",
5
+ "keywords": [
6
+ "alchemy",
7
+ "containers",
8
+ "docker",
9
+ "effect",
10
+ "infrastructure-as-code"
11
+ ],
12
+ "homepage": "https://alchemy.run",
13
+ "bugs": {
14
+ "url": "https://github.com/alchemy-run/alchemy/issues"
15
+ },
16
+ "license": "Apache-2.0",
17
+ "author": "Sam Goodwin <sam@alchemy.run>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/alchemy-run/alchemy.git",
21
+ "directory": "packages/docker"
22
+ },
23
+ "files": [
24
+ "lib",
25
+ "src"
26
+ ],
27
+ "type": "module",
28
+ "sideEffects": false,
29
+ "exports": {
30
+ ".": {
31
+ "types": "./lib/index.d.ts",
32
+ "bun": "./src/index.ts",
33
+ "default": "./lib/index.js"
34
+ },
35
+ "./Bootstrap": {
36
+ "types": "./src/Bootstrap.ts",
37
+ "bun": "./src/Bootstrap.ts",
38
+ "default": "./src/Bootstrap.ts"
39
+ },
40
+ "./*": {
41
+ "types": "./lib/*.d.ts",
42
+ "bun": "./src/*.ts",
43
+ "default": "./lib/*.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "dependencies": {
51
+ "rolldown": "1.2.13"
52
+ },
53
+ "devDependencies": {
54
+ "@effect/platform-bun": "^4.0.5",
55
+ "@types/bun": "1.4.3",
56
+ "@types/node": "26.6.5",
57
+ "alchemy": "2.0.0-beta.84",
58
+ "effect": "^4.0.5",
59
+ "typescript": "^7.0.2"
60
+ },
61
+ "peerDependencies": {
62
+ "@effect/platform-bun": "^4.0.5",
63
+ "alchemy": "2.0.0-beta.84",
64
+ "effect": "^4.0.5"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@effect/platform-bun": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "scripts": {
72
+ "build": "pnpm -r --filter \"{.}...\" run build:package",
73
+ "build:package": "node scripts/build.ts"
74
+ }
6
75
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Process bootstrap for `Docker.Service` (a bun process serving the
3
+ * bundled program). The generated entry imports this module and the user's
4
+ * `main`, nothing else — see `alchemy/Runtime/Bootstrap` for why.
5
+ */
6
+ import { BunServices } from "@effect/platform-bun";
7
+ import { BunHttpServer } from "alchemy/Http";
8
+ import { reifyBoundConfigProvider } from "alchemy/Runtime";
9
+ import {
10
+ entrypointLayer,
11
+ resolveProgram,
12
+ runProcess,
13
+ stackFromEnv,
14
+ } from "alchemy/Runtime/Bootstrap";
15
+ import * as ConfigProvider from "effect/ConfigProvider";
16
+ import * as Effect from "effect/Effect";
17
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
18
+ import * as Layer from "effect/Layer";
19
+ import * as Logger from "effect/Logger";
20
+
21
+ /**
22
+ * Resolve the bundled program (the runners registered via `host.run` /
23
+ * serve) and run it with a Bun HTTP server bound to `PORT`, so the returned
24
+ * `{ fetch }` handler is actually served and `host.run` loops stay alive.
25
+ */
26
+ export const bootstrap = (entrypoint: unknown): Promise<void> => {
27
+ const platform = Layer.mergeAll(
28
+ BunServices.layer,
29
+ FetchHttpClient.layer,
30
+ Logger.layer([Logger.consolePretty()]),
31
+ );
32
+
33
+ const program = resolveProgram("program").pipe(
34
+ Effect.provide(
35
+ entrypointLayer(entrypoint).pipe(
36
+ Layer.provideMerge(stackFromEnv),
37
+ Layer.provideMerge(BunHttpServer()),
38
+ Layer.provideMerge(platform),
39
+ Layer.provideMerge(
40
+ Layer.succeed(
41
+ ConfigProvider.ConfigProvider,
42
+ reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
43
+ ),
44
+ ),
45
+ ),
46
+ ),
47
+ Effect.scoped,
48
+ );
49
+
50
+ return runProcess("Docker service", program);
51
+ };
@@ -0,0 +1,255 @@
1
+ /**
2
+ * INTERNAL — deterministic identity for a Docker build: context + Dockerfile
3
+ * + platform + build args, with Docker's own semantics (`.dockerignore`
4
+ * pattern matching, symlink targets, permission bits, empty directories).
5
+ * Cloud-agnostic — consumed by `AWS.ECR.Image` and `AWS.Lambda.Function`
6
+ * image packaging; NOT exported from the Docker barrel.
7
+ */
8
+
9
+ import * as crypto from "node:crypto";
10
+ import {
11
+ type IgnoreRules,
12
+ listFileSystemDirectory,
13
+ normalizeRelativePath,
14
+ parseIgnoreRules,
15
+ walkIgnoring,
16
+ } from "alchemy/Util/Ignore";
17
+ import * as Effect from "effect/Effect";
18
+ import * as FileSystem from "effect/FileSystem";
19
+ import * as Path from "effect/Path";
20
+ import * as Result from "effect/Result";
21
+ import * as Stream from "effect/Stream";
22
+
23
+ export interface DockerBuildSource {
24
+ context: string;
25
+ dockerfile: string;
26
+ platform: string;
27
+ buildArgs?: Record<string, string>;
28
+ }
29
+
30
+ export type DockerBuildHashMode = "all" | "effective";
31
+
32
+ interface DockerIgnore {
33
+ content: string;
34
+ path?: string;
35
+ rules: IgnoreRules;
36
+ }
37
+
38
+ /**
39
+ * Resolve and validate a Docker build context and Dockerfile.
40
+ *
41
+ * Dockerfile paths are relative to the build context unless absolute.
42
+ */
43
+ export const resolveDockerBuildPaths = Effect.fn(function* (
44
+ source: Pick<DockerBuildSource, "context" | "dockerfile">,
45
+ ) {
46
+ const fs = yield* FileSystem.FileSystem;
47
+ const path = yield* Path.Path;
48
+ const context = path.resolve(source.context);
49
+ const dockerfile = path.isAbsolute(source.dockerfile)
50
+ ? source.dockerfile
51
+ : path.resolve(context, source.dockerfile);
52
+
53
+ if (!(yield* fs.exists(context))) {
54
+ return yield* Effect.fail(new Error(`Docker build context does not exist: ${context}`));
55
+ }
56
+ if (!(yield* fs.exists(dockerfile))) {
57
+ return yield* Effect.fail(new Error(`Dockerfile does not exist: ${dockerfile}`));
58
+ }
59
+
60
+ return { context, dockerfile };
61
+ });
62
+
63
+ const resolveDockerIgnore = Effect.fn(function* ({
64
+ context,
65
+ dockerfile,
66
+ }: {
67
+ context: string;
68
+ dockerfile: string;
69
+ }) {
70
+ const fs = yield* FileSystem.FileSystem;
71
+ const path = yield* Path.Path;
72
+ const dockerfileIgnore = `${dockerfile}.dockerignore`;
73
+ const contextIgnore = path.join(context, ".dockerignore");
74
+ const ignoreFile = (yield* fs.exists(dockerfileIgnore))
75
+ ? dockerfileIgnore
76
+ : (yield* fs.exists(contextIgnore))
77
+ ? contextIgnore
78
+ : undefined;
79
+
80
+ if (ignoreFile === undefined) {
81
+ return undefined;
82
+ }
83
+
84
+ const content = yield* fs.readFileString(ignoreFile);
85
+ const relativePath = normalizeRelativePath(path.relative(context, ignoreFile));
86
+ return {
87
+ content,
88
+ path:
89
+ relativePath === ".." || relativePath.startsWith("../") || path.isAbsolute(relativePath)
90
+ ? undefined
91
+ : relativePath,
92
+ rules: parseIgnoreRules(content, "dockerignore"),
93
+ } satisfies DockerIgnore;
94
+ });
95
+
96
+ interface DockerBuildContextSelection {
97
+ /** Absolute build-context directory. */
98
+ readonly context: string;
99
+ /** Absolute Dockerfile path. */
100
+ readonly dockerfile: string;
101
+ /** Dockerfile path relative to the context, if it is inside the context. */
102
+ readonly dockerfilePath: string | undefined;
103
+ /** Return whether an entry must be sent to the Docker builder. */
104
+ readonly includes: (relativePath: string) => boolean;
105
+ }
106
+
107
+ /**
108
+ * Select the effective files sent for a Docker build.
109
+ *
110
+ * Dockerfile-specific ignore files take precedence over `.dockerignore`.
111
+ * The Dockerfile and selected ignore file stay in the upload even when an
112
+ * ignore rule matches them, as Docker clients must send both to the builder.
113
+ */
114
+ export const selectDockerBuildContext = Effect.fn(function* (
115
+ source: Pick<DockerBuildSource, "context" | "dockerfile">,
116
+ ) {
117
+ const path = yield* Path.Path;
118
+ const { context, dockerfile } = yield* resolveDockerBuildPaths(source);
119
+ const dockerignore = yield* resolveDockerIgnore({ context, dockerfile });
120
+ const relativeDockerfile = normalizeRelativePath(path.relative(context, dockerfile));
121
+ const dockerfilePath =
122
+ relativeDockerfile === ".." ||
123
+ relativeDockerfile.startsWith("../") ||
124
+ path.isAbsolute(relativeDockerfile)
125
+ ? undefined
126
+ : relativeDockerfile;
127
+
128
+ return {
129
+ context,
130
+ dockerfile,
131
+ dockerfilePath,
132
+ includes: (relativePath: string) => {
133
+ const normalized = normalizeRelativePath(relativePath);
134
+ if (normalized === dockerfilePath || normalized === dockerignore?.path) {
135
+ return true;
136
+ }
137
+ return dockerignore === undefined || !dockerignore.rules.ignores(normalized);
138
+ },
139
+ } satisfies DockerBuildContextSelection;
140
+ });
141
+
142
+ /**
143
+ * Hash a Docker build context together with the Dockerfile, platform, and
144
+ * build arguments. In `effective` mode, the selected `.dockerignore` is
145
+ * applied exactly once before files are hashed. Absolute paths do not
146
+ * participate.
147
+ */
148
+ export const hashDockerBuildInputs = Effect.fn(function* (
149
+ source: DockerBuildSource,
150
+ mode: DockerBuildHashMode,
151
+ ) {
152
+ const fs = yield* FileSystem.FileSystem;
153
+ const path = yield* Path.Path;
154
+ const { context, dockerfile } = yield* resolveDockerBuildPaths(source);
155
+ const dockerignore =
156
+ mode === "effective" ? yield* resolveDockerIgnore({ context, dockerfile }) : undefined;
157
+ const hasher = yield* Effect.sync(() => crypto.createHash("sha256"));
158
+
159
+ yield* Effect.sync(() =>
160
+ hasher.update(
161
+ JSON.stringify({
162
+ platform: source.platform,
163
+ buildArgs: Object.entries(source.buildArgs ?? {}).sort(([a], [b]) =>
164
+ a < b ? -1 : a > b ? 1 : 0,
165
+ ),
166
+ dockerignore:
167
+ dockerignore === undefined
168
+ ? undefined
169
+ : {
170
+ content: dockerignore.content,
171
+ path: dockerignore.path,
172
+ },
173
+ }),
174
+ ),
175
+ );
176
+
177
+ const dockerfileContent = yield* fs.readFile(dockerfile);
178
+ yield* Effect.sync(() => {
179
+ hasher.update("Dockerfile\0");
180
+ hasher.update(dockerfileContent);
181
+ });
182
+
183
+ const forcedPaths = [
184
+ dockerignore?.path,
185
+ normalizeRelativePath(path.relative(context, dockerfile)),
186
+ ].filter(
187
+ (entry): entry is string =>
188
+ entry !== undefined && entry !== ".." && !entry.startsWith("../") && !path.isAbsolute(entry),
189
+ );
190
+ // Walk one level at a time so an excluded directory is discarded before
191
+ // its contents are read; entries come back sorted, preserving hash order.
192
+ const entries = yield* walkIgnoring({
193
+ list: yield* listFileSystemDirectory(context),
194
+ rules: dockerignore?.rules,
195
+ force: forcedPaths,
196
+ });
197
+
198
+ for (const { path: entry } of entries) {
199
+ // The ignore file and Dockerfile contents are hashed above; excluded
200
+ // entries were only walked so re-included descendants could be found.
201
+ if (
202
+ dockerignore !== undefined &&
203
+ (entry === dockerignore.path || dockerignore.rules.ignores(entry))
204
+ ) {
205
+ continue;
206
+ }
207
+
208
+ 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)) {
215
+ yield* Effect.sync(() =>
216
+ hasher.update(
217
+ `${JSON.stringify({
218
+ path: hashedEntry,
219
+ type: "SymbolicLink",
220
+ target: link.success,
221
+ })}\0`,
222
+ ),
223
+ );
224
+ continue;
225
+ }
226
+
227
+ const info = yield* fs.stat(fullPath);
228
+
229
+ // Docker COPY preserves entry types and permission bits, including empty
230
+ // directories. Include that metadata before any file bytes so changes such
231
+ // as making a Lambda bootstrap executable invalidate the image tag.
232
+ yield* Effect.sync(() =>
233
+ hasher.update(
234
+ `${JSON.stringify({
235
+ path: hashedEntry,
236
+ type: info.type,
237
+ mode: info.mode & 0o7777,
238
+ size: info.type === "File" ? String(info.size) : undefined,
239
+ })}\0`,
240
+ ),
241
+ );
242
+
243
+ if (info.type === "File") {
244
+ yield* fs.stream(fullPath).pipe(
245
+ Stream.runForEach((chunk) =>
246
+ Effect.sync(() => {
247
+ hasher.update(chunk);
248
+ }),
249
+ ),
250
+ );
251
+ }
252
+ }
253
+
254
+ return (yield* Effect.sync(() => hasher.digest("hex"))).slice(0, 32);
255
+ });