@crewhaus/docker-images 0.1.8 → 0.2.0

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/dist/index.d.ts CHANGED
@@ -15,6 +15,13 @@
15
15
  * version` succeeding); injectable `runner` for tests.
16
16
  * - `loadDigests()` / `recordDigest()` over a `docker/digests.json`
17
17
  * lockfile so reproducible builds work offline.
18
+ * - `resolveImageDigest()` + `buildImageAndRecord()` — item 47 closes the
19
+ * digests.json loop: PUSHED builds record their registry manifest digest
20
+ * (the only digest `docker pull repo@sha256:…` accepts), and the release
21
+ * workflow records the pushed ghcr.io digests for each v* tag. A local
22
+ * `--load` build records NOTHING — `docker image inspect`'s `.Id` is the
23
+ * image CONFIG digest, which no registry can serve, so writing it into
24
+ * the lockfile would hand users an unpullable pin.
18
25
  */
19
26
  import { CrewhausError } from "@crewhaus/errors";
20
27
  export declare class DockerImagesError extends CrewhausError {
@@ -70,6 +77,54 @@ export type DigestsFile = {
70
77
  };
71
78
  export declare function loadDigests(path?: string): DigestsFile;
72
79
  export declare function recordDigest(target: TargetShape, tag: string, sha256: string, path?: string): void;
80
+ export type ResolveImageDigestOptions = {
81
+ readonly target: TargetShape;
82
+ readonly tag: string;
83
+ /** Test injection point — defaults to the same spawn runner as buildImage. */
84
+ readonly runner?: BuildRunner;
85
+ };
86
+ /**
87
+ * Resolve a PUSHED image's registry manifest digest via `docker buildx
88
+ * imagetools inspect`. Only the manifest digest is pullable
89
+ * (`docker pull repo@sha256:…`); the local image store's `.Id` is the image
90
+ * CONFIG digest, which registries do not serve — so there is deliberately no
91
+ * local-inspect fallback here (a `--load` build has no recordable digest).
92
+ */
93
+ export declare function resolveImageDigest(opts: ResolveImageDigestOptions): Promise<string>;
94
+ export type BuildImageAndRecordOptions = BuildImageOptions & {
95
+ /** Record the built image's digest into docker/digests.json. Default true. */
96
+ readonly record?: boolean;
97
+ /** Override the digests.json location (tests / CI checkouts). */
98
+ readonly digestsFile?: string;
99
+ /** Test injection point for the recording seam. Defaults to {@link recordDigest}. */
100
+ readonly recordDigestFn?: typeof recordDigest;
101
+ };
102
+ export type BuildImageAndRecordResult = BuildImageResult & {
103
+ /** Registry manifest digest of the pushed image; undefined when recording was skipped or failed. */
104
+ readonly digest?: string;
105
+ readonly recorded: boolean;
106
+ /** Why the digest was not recorded (set only when recording was requested but failed). */
107
+ readonly recordError?: string;
108
+ /**
109
+ * Set when recording was requested but is not applicable: a `--load` build
110
+ * only produces a local image whose ID is a CONFIG digest, not a pullable
111
+ * registry manifest digest, so nothing truthful can land in the lockfile.
112
+ */
113
+ readonly recordSkippedReason?: string;
114
+ };
115
+ /**
116
+ * `buildImage()` + close the digests.json loop: after a successful PUSHED
117
+ * build, resolve the image's registry manifest digest (`docker buildx
118
+ * imagetools inspect`) and record it in the lockfile — the maintenance the
119
+ * digests.json header always promised. `crewhaus build-image --push` calls
120
+ * this (recording is default-on there; `--no-record` opts out). A local
121
+ * `--load` build records NOTHING (see `recordSkippedReason`): its image ID is
122
+ * a config digest that `docker pull repo@sha256:…` cannot resolve, and the
123
+ * lockfile must only ever contain pullable pins. A digest-resolution/record
124
+ * failure does NOT fail the build — the image exists; the caller decides how
125
+ * loudly to surface the stale lockfile (the CLI prints a warning).
126
+ */
127
+ export declare function buildImageAndRecord(opts: BuildImageAndRecordOptions): Promise<BuildImageAndRecordResult>;
73
128
  export type DockerfileFingerprint = {
74
129
  readonly stages: ReadonlyArray<{
75
130
  readonly image: string;
package/dist/index.js CHANGED
@@ -18,6 +18,13 @@ import { fileURLToPath } from "node:url";
18
18
  * version` succeeding); injectable `runner` for tests.
19
19
  * - `loadDigests()` / `recordDigest()` over a `docker/digests.json`
20
20
  * lockfile so reproducible builds work offline.
21
+ * - `resolveImageDigest()` + `buildImageAndRecord()` — item 47 closes the
22
+ * digests.json loop: PUSHED builds record their registry manifest digest
23
+ * (the only digest `docker pull repo@sha256:…` accepts), and the release
24
+ * workflow records the pushed ghcr.io digests for each v* tag. A local
25
+ * `--load` build records NOTHING — `docker image inspect`'s `.Id` is the
26
+ * image CONFIG digest, which no registry can serve, so writing it into
27
+ * the lockfile would hand users an unpullable pin.
21
28
  */
22
29
  import { CrewhausError } from "@crewhaus/errors";
23
30
  export class DockerImagesError extends CrewhausError {
@@ -148,6 +155,66 @@ export function recordDigest(target, tag, sha256, path = DIGESTS_PATH) {
148
155
  mkdirSync(dirname(path), { recursive: true });
149
156
  writeFileSync(path, `${JSON.stringify(current, null, 2)}\n`, { mode: 0o644 });
150
157
  }
158
+ const SHA256_DIGEST_RE = /sha256:[0-9a-f]{64}/;
159
+ /**
160
+ * Resolve a PUSHED image's registry manifest digest via `docker buildx
161
+ * imagetools inspect`. Only the manifest digest is pullable
162
+ * (`docker pull repo@sha256:…`); the local image store's `.Id` is the image
163
+ * CONFIG digest, which registries do not serve — so there is deliberately no
164
+ * local-inspect fallback here (a `--load` build has no recordable digest).
165
+ */
166
+ export async function resolveImageDigest(opts) {
167
+ const ref = `crewhaus/${opts.target}:${opts.tag}`;
168
+ const argv = ["docker", "buildx", "imagetools", "inspect", ref];
169
+ const runner = opts.runner ?? defaultRunner;
170
+ const { exitCode, stdout, stderr } = await runner(argv, process.cwd());
171
+ if (exitCode !== 0) {
172
+ throw new DockerImagesError(`could not inspect ${ref} for its digest (exit ${exitCode}): ${stderr.slice(0, 512)}`);
173
+ }
174
+ const match = SHA256_DIGEST_RE.exec(stdout);
175
+ if (match === null) {
176
+ throw new DockerImagesError(`no sha256 digest in docker inspect output for ${ref}`);
177
+ }
178
+ return match[0];
179
+ }
180
+ /**
181
+ * `buildImage()` + close the digests.json loop: after a successful PUSHED
182
+ * build, resolve the image's registry manifest digest (`docker buildx
183
+ * imagetools inspect`) and record it in the lockfile — the maintenance the
184
+ * digests.json header always promised. `crewhaus build-image --push` calls
185
+ * this (recording is default-on there; `--no-record` opts out). A local
186
+ * `--load` build records NOTHING (see `recordSkippedReason`): its image ID is
187
+ * a config digest that `docker pull repo@sha256:…` cannot resolve, and the
188
+ * lockfile must only ever contain pullable pins. A digest-resolution/record
189
+ * failure does NOT fail the build — the image exists; the caller decides how
190
+ * loudly to surface the stale lockfile (the CLI prints a warning).
191
+ */
192
+ export async function buildImageAndRecord(opts) {
193
+ const result = await buildImage(opts);
194
+ if (opts.record === false) {
195
+ return { ...result, recorded: false };
196
+ }
197
+ if (opts.push !== true) {
198
+ return {
199
+ ...result,
200
+ recorded: false,
201
+ recordSkippedReason: "local image ID is not a registry digest — use --push to record a pullable digest",
202
+ };
203
+ }
204
+ const record = opts.recordDigestFn ?? recordDigest;
205
+ try {
206
+ const digest = await resolveImageDigest({
207
+ target: opts.target,
208
+ tag: opts.tag,
209
+ runner: opts.runner,
210
+ });
211
+ record(opts.target, opts.tag, digest, opts.digestsFile);
212
+ return { ...result, digest, recorded: true };
213
+ }
214
+ catch (err) {
215
+ return { ...result, recorded: false, recordError: err.message };
216
+ }
217
+ }
151
218
  export function fingerprintDockerfile(text) {
152
219
  const lines = text.split(/\r?\n/);
153
220
  const fromStages = lines
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/docker-images",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Per-target-shape Docker images (multi-stage, non-root, healthchecked) + crewhaus build-image CLI wrapper (Section 32)",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "test": "bun test src"
16
16
  },
17
17
  "dependencies": {
18
- "@crewhaus/errors": "0.1.8"
18
+ "@crewhaus/errors": "0.2.0"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {