@olivervea/pipelines 2.0.0-beta.18 → 2.0.0-beta.19

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 (51) hide show
  1. package/package.json +4 -2
  2. package/src/assets.d.ts +2 -2
  3. package/src/build/build.ts +111 -43
  4. package/src/build/cli.ts +28 -21
  5. package/src/build/elf.ts +8 -7
  6. package/src/build/hashes.ts +61 -28
  7. package/src/build/project.ts +69 -30
  8. package/src/builder/pipeline.ts +274 -102
  9. package/src/builder/refs.ts +12 -6
  10. package/src/core/brands.ts +6 -6
  11. package/src/core/config-refs.ts +46 -20
  12. package/src/core/config.ts +110 -33
  13. package/src/core/external.ts +30 -12
  14. package/src/core/handlers.ts +54 -14
  15. package/src/core/models.ts +29 -14
  16. package/src/core/origin.ts +7 -4
  17. package/src/core/secrets.ts +2 -2
  18. package/src/core/steps.ts +39 -23
  19. package/src/core/types.ts +5 -1
  20. package/src/core/webhooks.ts +21 -7
  21. package/src/ctx/files.ts +111 -40
  22. package/src/ctx/index.ts +75 -34
  23. package/src/ctx/sh.ts +104 -44
  24. package/src/ctx/types.ts +4 -4
  25. package/src/describe/describe.ts +550 -196
  26. package/src/describe/manifest-schema.ts +210 -80
  27. package/src/describe/mapper-source.ts +249 -104
  28. package/src/describe/record.ts +67 -37
  29. package/src/dev/cli.ts +64 -34
  30. package/src/harness/env.ts +33 -27
  31. package/src/harness/execute.ts +55 -29
  32. package/src/harness/index.ts +21 -8
  33. package/src/harness/main.ts +197 -74
  34. package/src/harness/result.ts +38 -22
  35. package/src/harness/validate.ts +62 -19
  36. package/src/index.ts +123 -14
  37. package/src/library/checks.ts +11 -6
  38. package/src/library/compose-deploy.ts +40 -23
  39. package/src/library/github-source.ts +69 -39
  40. package/src/library/helm-deploy-ssh.ts +213 -76
  41. package/src/library/helm-deploy.ts +263 -85
  42. package/src/library/http-health.ts +7 -5
  43. package/src/library/image-build.ts +293 -117
  44. package/src/library/index.ts +47 -15
  45. package/src/library/models.ts +17 -9
  46. package/src/library/ssh.ts +31 -14
  47. package/src/testing/index.ts +63 -24
  48. package/src/util/canonical.ts +7 -4
  49. package/src/util/handler-hash.ts +15 -5
  50. package/src/util/hash.ts +5 -5
  51. package/src/util/tar.ts +74 -37
@@ -1,51 +1,96 @@
1
1
  // Helm deploys to a target from config (`helmTarget()`): steps.helmDeployInCluster runs helm in the step
2
2
  // pod against its own cluster; steps.helmDeployOverSsh({ target, … }) runs it on a k3s host. Pick one
3
3
  // per target kind with `select`. Images come from a registry, pinned by digest.
4
- import { existsSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { z } from "zod";
7
- import type { Ctx } from "../ctx/types.ts";
8
- import type { Configurable } from "../core/config.ts";
9
- import { resolveConfigRefs } from "../core/config-refs.ts";
10
- import { implement, type Implementation } from "../core/handlers.ts";
11
- import type { FileRef } from "../core/models.ts";
12
- import { optional } from "../core/models.ts";
13
- import type { Secret } from "../core/secrets.ts";
14
- import { secretRef } from "../core/config.ts";
15
- import { step, type AnyStep, type LiteralName, type Step } from "../core/steps.ts";
16
- import type { Simplify } from "../core/types.ts";
17
- import { serviceUrl } from "./checks.ts";
18
- import { chartDirectory, deployOverSsh, DEFAULT_ROLLOUT_TIMEOUT_SECONDS, HELM_DEPLOY_IMAGE, helmFlags, type HelmImageFlags, type HelmImageRefFlags } from "./helm-deploy-ssh.ts";
19
- import { httpHealthCheck } from "./http-health.ts";
20
- import { HelmRelease, PushedImage, Source } from "./models.ts";
4
+ import { existsSync, writeFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { z } from 'zod';
7
+ import type { Ctx } from '../ctx/types.ts';
8
+ import type { Configurable } from '../core/config.ts';
9
+ import { resolveConfigRefs } from '../core/config-refs.ts';
10
+ import { implement, type Implementation } from '../core/handlers.ts';
11
+ import type { FileRef } from '../core/models.ts';
12
+ import { optional } from '../core/models.ts';
13
+ import type { Secret } from '../core/secrets.ts';
14
+ import { secretRef } from '../core/config.ts';
15
+ import { step, type AnyStep, type LiteralName, type Step } from '../core/steps.ts';
16
+ import type { Simplify } from '../core/types.ts';
17
+ import { serviceUrl } from './checks.ts';
18
+ import {
19
+ chartDirectory,
20
+ deployOverSsh,
21
+ DEFAULT_ROLLOUT_TIMEOUT_SECONDS,
22
+ HELM_DEPLOY_IMAGE,
23
+ helmFlags,
24
+ type HelmImageFlags,
25
+ type HelmImageRefFlags,
26
+ } from './helm-deploy-ssh.ts';
27
+ import { httpHealthCheck } from './http-health.ts';
28
+ import { HelmRelease, PushedImage, Source } from './models.ts';
21
29
 
22
30
  /** helm 3 on Alpine, for in-cluster deploys (the health check runs in the harness). */
23
- export const HELM_IMAGE = "alpine/helm:3.16.2";
31
+ export const HELM_IMAGE = 'alpine/helm:3.16.2';
24
32
 
25
33
  /** Where the step pod finds its ServiceAccount token (mounted only with the instance's step ServiceAccount). */
26
- export const SERVICE_ACCOUNT_TOKEN = "/var/run/secrets/kubernetes.io/serviceaccount/token";
34
+ export const SERVICE_ACCOUNT_TOKEN =
35
+ '/var/run/secrets/kubernetes.io/serviceaccount/token';
27
36
 
28
- function targetVariants<const E extends z.ZodRawShape, const K extends string>(o: Omit<DeployTargetOptions<E, K>, "composeToken"> & { composeToken?: Secret<K> }) {
29
- const name = (d: string | undefined) => (d === undefined ? z.string() : z.string().default(d));
37
+ function targetVariants<const E extends z.ZodRawShape, const K extends string>(
38
+ o: Omit<DeployTargetOptions<E, K>, 'composeToken'> & { composeToken?: Secret<K> },
39
+ ) {
40
+ const name = (d: string | undefined) =>
41
+ d === undefined ? z.string() : z.string().default(d);
30
42
  const common = {
31
- namespace: name(o.namespace).describe("Namespace the release is installed into."),
32
- release: name(o.release).describe("Helm release name."),
33
- values: z.array(z.string()).default([]).describe("Values files, relative to the chart directory (-f, in order)."),
34
- set: z.record(z.string(), z.string()).default({}).describe("Extra --set flags."),
35
- url: z.string().optional().describe("Base URL step pods reach the release at (health checks, smoke tests); default http://<release>.<namespace>.svc.cluster.local."),
43
+ namespace: name(o.namespace).describe('Namespace the release is installed into.'),
44
+ release: name(o.release).describe('Helm release name.'),
45
+ values: z
46
+ .array(z.string())
47
+ .default([])
48
+ .describe('Values files, relative to the chart directory (-f, in order).'),
49
+ set: z.record(z.string(), z.string()).default({}).describe('Extra --set flags.'),
50
+ url: z
51
+ .string()
52
+ .optional()
53
+ .describe(
54
+ 'Base URL step pods reach the release at (health checks, smoke tests); default http://<release>.<namespace>.svc.cluster.local.',
55
+ ),
36
56
  ...(o.extend ?? ({} as E)),
37
57
  };
38
- const inCluster = z.object({ kind: z.literal("inCluster").describe("helm runs in the step pod against its own cluster (needs the instance's step ServiceAccount)."), ...common });
39
- const ssh = z.object({ kind: z.literal("ssh").describe("helm runs on a k3s host over SSH."), host: z.string().describe("user@host"), ...common });
58
+ const inCluster = z.object({
59
+ kind: z
60
+ .literal('inCluster')
61
+ .describe(
62
+ "helm runs in the step pod against its own cluster (needs the instance's step ServiceAccount).",
63
+ ),
64
+ ...common,
65
+ });
66
+ const ssh = z.object({
67
+ kind: z.literal('ssh').describe('helm runs on a k3s host over SSH.'),
68
+ host: z.string().describe('user@host'),
69
+ ...common,
70
+ });
40
71
  const compose = z.object({
41
- kind: z.literal("compose").describe("A Docker Compose deployer recreates a service with the image."),
42
- deployerUrl: z.string().describe("The deployer's base URL (POST <deployerUrl>/v1/deploy)."),
43
- service: z.string().describe("The compose service to recreate."),
44
- token: secretRef(o.composeToken ?? ({ kind: "secret", name: "DEPLOYER_TOKEN", description: "" } as Secret<K>)).describe("The deployer's bearer token."),
45
- healthUrl: z.string().optional().describe("Checked by the step after the deployer's own health check, when the step can reach it."),
72
+ kind: z
73
+ .literal('compose')
74
+ .describe('A Docker Compose deployer recreates a service with the image.'),
75
+ deployerUrl: z
76
+ .string()
77
+ .describe("The deployer's base URL (POST <deployerUrl>/v1/deploy)."),
78
+ service: z.string().describe('The compose service to recreate.'),
79
+ token: secretRef(
80
+ o.composeToken ??
81
+ ({ kind: 'secret', name: 'DEPLOYER_TOKEN', description: '' } as Secret<K>),
82
+ ).describe("The deployer's bearer token."),
83
+ healthUrl: z
84
+ .string()
85
+ .optional()
86
+ .describe(
87
+ "Checked by the step after the deployer's own health check, when the step can reach it.",
88
+ ),
46
89
  ...(o.extend ?? ({} as E)),
47
90
  });
48
- const none = z.object({ kind: z.literal("none").describe("Not deployed: the target's steps are left out.") });
91
+ const none = z.object({
92
+ kind: z.literal('none').describe("Not deployed: the target's steps are left out."),
93
+ });
49
94
  return { inCluster, ssh, compose, none };
50
95
  }
51
96
 
@@ -67,21 +112,42 @@ export interface DeployTargetOptions<E extends z.ZodRawShape, K extends string>
67
112
  * `compose` (a Docker Compose deployer), and with `optional: true` also `none` (not deployed).
68
113
  * `config("target", "…", z.object({ beta: deployTarget({ optional: true, composeToken: T }), prod: deployTarget({ composeToken: T }) }))`
69
114
  */
70
- export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(o: DeployTargetOptions<E, K> & { optional: true }): z.ZodDiscriminatedUnion<[
71
- ReturnType<typeof targetVariants<E, K>>["inCluster"], ReturnType<typeof targetVariants<E, K>>["ssh"], ReturnType<typeof targetVariants<E, K>>["compose"], ReturnType<typeof targetVariants<E, K>>["none"],
72
- ]>;
73
- export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(o: DeployTargetOptions<E, K>): z.ZodDiscriminatedUnion<[
74
- ReturnType<typeof targetVariants<E, K>>["inCluster"], ReturnType<typeof targetVariants<E, K>>["ssh"], ReturnType<typeof targetVariants<E, K>>["compose"],
75
- ]>;
115
+ export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(
116
+ o: DeployTargetOptions<E, K> & { optional: true },
117
+ ): z.ZodDiscriminatedUnion<
118
+ [
119
+ ReturnType<typeof targetVariants<E, K>>['inCluster'],
120
+ ReturnType<typeof targetVariants<E, K>>['ssh'],
121
+ ReturnType<typeof targetVariants<E, K>>['compose'],
122
+ ReturnType<typeof targetVariants<E, K>>['none'],
123
+ ]
124
+ >;
125
+ export function deployTarget<const K extends string, const E extends z.ZodRawShape = {}>(
126
+ o: DeployTargetOptions<E, K>,
127
+ ): z.ZodDiscriminatedUnion<
128
+ [
129
+ ReturnType<typeof targetVariants<E, K>>['inCluster'],
130
+ ReturnType<typeof targetVariants<E, K>>['ssh'],
131
+ ReturnType<typeof targetVariants<E, K>>['compose'],
132
+ ]
133
+ >;
76
134
  export function deployTarget(o: DeployTargetOptions<z.ZodRawShape, string>): unknown {
77
135
  const v = targetVariants(o);
78
- return z.discriminatedUnion("kind", o.optional ? [v.inCluster, v.ssh, v.compose, v.none] : [v.inCluster, v.ssh, v.compose]);
136
+ return z.discriminatedUnion(
137
+ 'kind',
138
+ o.optional
139
+ ? [v.inCluster, v.ssh, v.compose, v.none]
140
+ : [v.inCluster, v.ssh, v.compose],
141
+ );
79
142
  }
80
143
 
81
144
  /** A helm-only deploy target (`inCluster | ssh`). */
82
- export function helmTarget<const E extends z.ZodRawShape = {}>(defaults: { namespace?: string; release?: string } = {}, extend?: E) {
145
+ export function helmTarget<const E extends z.ZodRawShape = {}>(
146
+ defaults: { namespace?: string; release?: string } = {},
147
+ extend?: E,
148
+ ) {
83
149
  const v = targetVariants({ ...defaults, extend });
84
- return z.discriminatedUnion("kind", [v.inCluster, v.ssh]);
150
+ return z.discriminatedUnion('kind', [v.inCluster, v.ssh]);
85
151
  }
86
152
 
87
153
  interface TargetCommon {
@@ -92,19 +158,28 @@ interface TargetCommon {
92
158
  url?: string | undefined;
93
159
  }
94
160
  /** A resolved `kind: inCluster` target. */
95
- export type InClusterTargetValue = TargetCommon & { kind: "inCluster" };
161
+ export type InClusterTargetValue = TargetCommon & { kind: 'inCluster' };
96
162
  /** A resolved `kind: ssh` target. */
97
- export type SshTargetValue = TargetCommon & { kind: "ssh"; host: string };
163
+ export type SshTargetValue = TargetCommon & { kind: 'ssh'; host: string };
98
164
  /** A resolved deploy target (the output of `helmTarget()`). */
99
165
  export type HelmTargetValue = InClusterTargetValue | SshTargetValue;
100
166
 
101
- type DefaultImages = { readonly image: "image" };
102
- export type HelmTargetDeployInput<Im extends Record<string, string> = DefaultImages, Rf extends Record<string, string> = {}> = Simplify<
103
- { readonly source: typeof Source } & { readonly [K in keyof Im]: typeof PushedImage } & { readonly [K in keyof Rf]: ReturnType<typeof optional<typeof PushedImage>> }
167
+ type DefaultImages = { readonly image: 'image' };
168
+ export type HelmTargetDeployInput<
169
+ Im extends Record<string, string> = DefaultImages,
170
+ Rf extends Record<string, string> = {},
171
+ > = Simplify<
172
+ { readonly source: typeof Source } & {
173
+ readonly [K in keyof Im]: typeof PushedImage;
174
+ } & { readonly [K in keyof Rf]: ReturnType<typeof optional<typeof PushedImage>> }
104
175
  >;
105
176
 
106
177
  /** What both target deploys take (besides `target`). */
107
- export interface HelmTargetDeployDef<N extends string, Im extends Record<string, string> = DefaultImages, Rf extends Record<string, string> = {}> {
178
+ export interface HelmTargetDeployDef<
179
+ N extends string,
180
+ Im extends Record<string, string> = DefaultImages,
181
+ Rf extends Record<string, string> = {},
182
+ > {
108
183
  name: LiteralName<N>;
109
184
  /** What the step does, shown in the UI. Required; may be empty. */
110
185
  description: string;
@@ -134,14 +209,21 @@ export interface HelmTargetDeployDef<N extends string, Im extends Record<string,
134
209
  timeoutSeconds?: number;
135
210
  }
136
211
 
137
- export interface HelmDeployInClusterDef<N extends string, Im extends Record<string, string> = DefaultImages, Rf extends Record<string, string> = {}>
138
- extends HelmTargetDeployDef<N, Im, Rf> {
212
+ export interface HelmDeployInClusterDef<
213
+ N extends string,
214
+ Im extends Record<string, string> = DefaultImages,
215
+ Rf extends Record<string, string> = {},
216
+ > extends HelmTargetDeployDef<N, Im, Rf> {
139
217
  /** An in-cluster target, usually a `select` case's reference (`(p, prod) => … target: prod`). */
140
218
  target: Configurable<InClusterTargetValue>;
141
219
  }
142
220
 
143
- export interface HelmDeploySshTargetDef<N extends string, K extends string, Im extends Record<string, string> = DefaultImages, Rf extends Record<string, string> = {}>
144
- extends HelmTargetDeployDef<N, Im, Rf> {
221
+ export interface HelmDeploySshTargetDef<
222
+ N extends string,
223
+ K extends string,
224
+ Im extends Record<string, string> = DefaultImages,
225
+ Rf extends Record<string, string> = {},
226
+ > extends HelmTargetDeployDef<N, Im, Rf> {
145
227
  /** An ssh target, usually a `select` case's reference. */
146
228
  target: Configurable<SshTargetValue>;
147
229
  /** Private key for the target's host. */
@@ -159,13 +241,31 @@ export function helmDeployInCluster<
159
241
  const N extends string,
160
242
  const Im extends Record<string, string> = DefaultImages,
161
243
  const Rf extends Record<string, string> = {},
162
- >(def: HelmDeployInClusterDef<N, Im, Rf>): Implementation<Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, never>> {
163
- return targetDeploy("helmDeployInCluster", def as unknown as AnyTargetDef, undefined) as never;
244
+ >(
245
+ def: HelmDeployInClusterDef<N, Im, Rf>,
246
+ ): Implementation<Step<N, HelmTargetDeployInput<Im, Rf>, typeof HelmRelease, never>> {
247
+ return targetDeploy(
248
+ 'helmDeployInCluster',
249
+ def as unknown as AnyTargetDef,
250
+ undefined,
251
+ ) as never;
164
252
  }
165
253
 
166
254
  /** The `target` form of `steps.helmDeployOverSsh`: an ssh target from config, registry images by digest. */
167
- export function helmDeployOverSshTarget(def: HelmDeploySshTargetDef<string, string, Record<string, string>, Record<string, string>>): Implementation {
168
- return targetDeploy("helmDeployOverSsh", def as unknown as AnyTargetDef, def.sshKey, def.deployments) as never;
255
+ export function helmDeployOverSshTarget(
256
+ def: HelmDeploySshTargetDef<
257
+ string,
258
+ string,
259
+ Record<string, string>,
260
+ Record<string, string>
261
+ >,
262
+ ): Implementation {
263
+ return targetDeploy(
264
+ 'helmDeployOverSsh',
265
+ def as unknown as AnyTargetDef,
266
+ def.sshKey,
267
+ def.deployments,
268
+ ) as never;
169
269
  }
170
270
 
171
271
  interface AnyTargetDef {
@@ -181,17 +281,27 @@ interface AnyTargetDef {
181
281
  timeoutSeconds?: number;
182
282
  }
183
283
 
184
- function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: AnyTargetDef, sshKey: Secret | undefined, deployments?: readonly string[]): Implementation {
185
- const images: Record<string, string> = { ...(def.images ?? { image: "image" }) };
284
+ function targetDeploy(
285
+ kind: 'helmDeployInCluster' | 'helmDeployOverSsh',
286
+ def: AnyTargetDef,
287
+ sshKey: Secret | undefined,
288
+ deployments?: readonly string[],
289
+ ): Implementation {
290
+ const images: Record<string, string> = { ...(def.images ?? { image: 'image' }) };
186
291
  const imageEntries = Object.entries(images);
187
292
  const imageRefs: Record<string, string> = { ...(def.imageRefs ?? {}) };
188
293
  const refEntries = Object.entries(imageRefs);
189
294
  const rolloutTimeout = def.rolloutTimeoutSeconds ?? DEFAULT_ROLLOUT_TIMEOUT_SECONDS;
190
- if ("source" in images || "source" in imageRefs) throw new Error(`${kind} '${def.name}': 'source' can't be an image input`);
191
- for (const [key] of refEntries) if (key in images) throw new Error(`${kind} '${def.name}': '${key}' is both in images and imageRefs`);
192
- if (deployments?.length === 0) throw new Error(`${kind} '${def.name}': deployments must not be empty`);
193
- if (def.healthPath !== undefined && !def.healthPath.startsWith("/")) throw new Error(`${kind} '${def.name}': healthPath must start with '/'`);
194
- const inCluster = kind === "helmDeployInCluster";
295
+ if ('source' in images || 'source' in imageRefs)
296
+ throw new Error(`${kind} '${def.name}': 'source' can't be an image input`);
297
+ for (const [key] of refEntries)
298
+ if (key in images)
299
+ throw new Error(`${kind} '${def.name}': '${key}' is both in images and imageRefs`);
300
+ if (deployments?.length === 0)
301
+ throw new Error(`${kind} '${def.name}': deployments must not be empty`);
302
+ if (def.healthPath !== undefined && !def.healthPath.startsWith('/'))
303
+ throw new Error(`${kind} '${def.name}': healthPath must start with '/'`);
304
+ const inCluster = kind === 'helmDeployInCluster';
195
305
  const input = {
196
306
  source: Source,
197
307
  ...Object.fromEntries(imageEntries.map(([key]) => [key, PushedImage])),
@@ -217,15 +327,26 @@ function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: An
217
327
  images: { ...images },
218
328
  ...(refEntries.length > 0 ? { imageRefs: { ...imageRefs } } : {}),
219
329
  ...(deployments ? { deployments: [...deployments] } : {}),
220
- ...(def.rolloutTimeoutSeconds !== undefined ? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds } : {}),
330
+ ...(def.rolloutTimeoutSeconds !== undefined
331
+ ? { rolloutTimeoutSeconds: def.rolloutTimeoutSeconds }
332
+ : {}),
221
333
  ...(def.healthPath !== undefined ? { healthPath: def.healthPath } : {}),
222
334
  },
223
335
  } as never) as AnyStep;
224
336
  return implement(contract, async (stepInput, ctx: Ctx) => {
225
- const input = stepInput as unknown as Record<string, { repository: string; tag: string; digest: string } | undefined> & { source: { tree: FileRef } };
226
- const { target, inlineValues } = resolveConfigRefs(configurable, ctx.config ?? {}) as { target: HelmTargetValue; inlineValues: Record<string, unknown>[] };
227
- const want = inCluster ? "inCluster" : "ssh";
228
- if (target?.kind !== want) throw new Error(`${kind} '${def.name}': the target is kind '${String(target?.kind)}', not '${want}'`);
337
+ const input = stepInput as unknown as Record<
338
+ string,
339
+ { repository: string; tag: string; digest: string } | undefined
340
+ > & { source: { tree: FileRef } };
341
+ const { target, inlineValues } = resolveConfigRefs(
342
+ configurable,
343
+ ctx.config ?? {},
344
+ ) as { target: HelmTargetValue; inlineValues: Record<string, unknown>[] };
345
+ const want = inCluster ? 'inCluster' : 'ssh';
346
+ if (target?.kind !== want)
347
+ throw new Error(
348
+ `${kind} '${def.name}': the target is kind '${String(target?.kind)}', not '${want}'`,
349
+ );
229
350
  const chartDir = chartDirectory(await ctx.files.fetch(input.source.tree), def.chart);
230
351
 
231
352
  const generated = inlineValues.map((values, i) => {
@@ -235,37 +356,94 @@ function targetDeploy(kind: "helmDeployInCluster" | "helmDeployOverSsh", def: An
235
356
  });
236
357
  const flags: HelmImageFlags[] = imageEntries.map(([key, prefix]) => {
237
358
  const image = input[key]!;
238
- return { prefix, repository: image.repository, tag: image.tag, digest: image.digest, pullPolicy: "IfNotPresent" };
359
+ return {
360
+ prefix,
361
+ repository: image.repository,
362
+ tag: image.tag,
363
+ digest: image.digest,
364
+ pullPolicy: 'IfNotPresent',
365
+ };
239
366
  });
240
367
  const refs: HelmImageRefFlags[] = refEntries.flatMap(([key, prefix]) => {
241
368
  const image = input[key];
242
- return image ? [{ prefix, repository: image.repository, tag: image.tag, digest: image.digest }] : [];
369
+ return image
370
+ ? [{ prefix, repository: image.repository, tag: image.tag, digest: image.digest }]
371
+ : [];
243
372
  });
244
- const helm = { values: [...target.values, ...generated], set: target.set, images: flags, imageRefs: refs };
373
+ const helm = {
374
+ values: [...target.values, ...generated],
375
+ set: target.set,
376
+ images: flags,
377
+ imageRefs: refs,
378
+ };
245
379
  const { release, namespace } = target;
246
380
 
247
- if (target.kind === "ssh") {
381
+ if (target.kind === 'ssh') {
248
382
  const key = ctx.secrets[sshKey!.name];
249
383
  if (!key) throw new Error(`secret ${sshKey!.name} (sshKey) is not set`);
250
384
  await deployOverSsh(ctx, {
251
- host: target.host, key, chartDir, release, namespace,
252
- hostImages: flags.map((f) => ({ repository: f.repository, tag: f.tag, digest: f.digest })),
253
- flags: helm, deployments: deployments ?? (imageEntries.length ? [release] : []), rolloutTimeoutSeconds: rolloutTimeout,
385
+ host: target.host,
386
+ key,
387
+ chartDir,
388
+ release,
389
+ namespace,
390
+ hostImages: flags.map((f) => ({
391
+ repository: f.repository,
392
+ tag: f.tag,
393
+ digest: f.digest,
394
+ })),
395
+ flags: helm,
396
+ deployments: deployments ?? (imageEntries.length ? [release] : []),
397
+ rolloutTimeoutSeconds: rolloutTimeout,
254
398
  });
255
399
  } else {
256
400
  if (!existsSync(SERVICE_ACCOUNT_TOKEN))
257
- throw new Error(`no ServiceAccount token in the step pod: the instance grants no step ServiceAccount (Kubernetes:StepServiceAccount), so an inCluster target can't be deployed`);
258
- const home = join(ctx.tmpDir, "helm-home");
259
- ctx.log("helm upgrade", { release, namespace, images: flags.map((f) => `${f.repository}@${f.digest}`).join(" ") });
260
- await ctx.exec(["helm", "upgrade", "--install", release, ".", "-n", namespace, ...helmFlags(helm), "--wait", `--timeout=${rolloutTimeout}s`], {
261
- cwd: chartDir,
262
- env: { HOME: home, HELM_CACHE_HOME: join(home, "cache"), HELM_CONFIG_HOME: join(home, "config"), HELM_DATA_HOME: join(home, "data") },
401
+ throw new Error(
402
+ `no ServiceAccount token in the step pod: the instance grants no step ServiceAccount (Kubernetes:StepServiceAccount), so an inCluster target can't be deployed`,
403
+ );
404
+ const home = join(ctx.tmpDir, 'helm-home');
405
+ ctx.log('helm upgrade', {
406
+ release,
407
+ namespace,
408
+ images: flags.map((f) => `${f.repository}@${f.digest}`).join(' '),
263
409
  });
410
+ await ctx.exec(
411
+ [
412
+ 'helm',
413
+ 'upgrade',
414
+ '--install',
415
+ release,
416
+ '.',
417
+ '-n',
418
+ namespace,
419
+ ...helmFlags(helm),
420
+ '--wait',
421
+ `--timeout=${rolloutTimeout}s`,
422
+ ],
423
+ {
424
+ cwd: chartDir,
425
+ env: {
426
+ HOME: home,
427
+ HELM_CACHE_HOME: join(home, 'cache'),
428
+ HELM_CONFIG_HOME: join(home, 'config'),
429
+ HELM_DATA_HOME: join(home, 'data'),
430
+ },
431
+ },
432
+ );
264
433
  }
265
434
 
266
- const url = (target.url ?? serviceUrl({ release, namespace })).replace(/\/+$/, "");
267
- if (def.healthPath !== undefined) await httpHealthCheck(ctx, `${url}${def.healthPath}`, { attempts: 12, delaySeconds: 5 });
435
+ const url = (target.url ?? serviceUrl({ release, namespace })).replace(/\/+$/, '');
436
+ if (def.healthPath !== undefined)
437
+ await httpHealthCheck(ctx, `${url}${def.healthPath}`, {
438
+ attempts: 12,
439
+ delaySeconds: 5,
440
+ });
268
441
  const main = flags[0];
269
- return { release, namespace, ...(main ? { image: `${main.repository}:${main.tag}@${main.digest}` } : {}), ...(target.url ? { url } : {}) } as never;
442
+ return {
443
+ release,
444
+ namespace,
445
+ ...(main ? { image: `${main.repository}:${main.tag}@${main.digest}` } : {}),
446
+ ...(target.url ? { url } : {}),
447
+ } as never;
270
448
  }) as never;
271
449
  }
@@ -3,7 +3,7 @@
3
3
  // The general HTTP health check (ported from v1's deploy-beta.sh curl loop). Library steps use it
4
4
  // (helmDeployOverSsh's `healthUrl`), and it is exported for custom handlers, which get it with
5
5
  // their own retry policy. Runs in the harness (Bun fetch), so the image needs no curl.
6
- import type { Ctx } from "../ctx/types.ts";
6
+ import type { Ctx } from '../ctx/types.ts';
7
7
 
8
8
  export interface HttpHealthCheckOptions {
9
9
  /** Total attempts (v1: 5). */
@@ -22,13 +22,15 @@ export const HTTP_HEALTH_DEFAULTS = { attempts: 5, delaySeconds: 5 } as const;
22
22
  * non-2xx statuses. Throws the last error (status + URL, query redacted) once the attempts run out.
23
23
  */
24
24
  export async function httpHealthCheck(
25
- ctx: Pick<Ctx, "http" | "retry" | "log">,
25
+ ctx: Pick<Ctx, 'http' | 'retry' | 'log'>,
26
26
  url: string,
27
27
  opts: HttpHealthCheckOptions = {},
28
28
  ): Promise<void> {
29
29
  const attempts = opts.attempts ?? HTTP_HEALTH_DEFAULTS.attempts;
30
30
  const delaySeconds = opts.delaySeconds ?? HTTP_HEALTH_DEFAULTS.delaySeconds;
31
- ctx.log("checking health", { url: url.split("?")[0]!, attempts });
32
- await ctx.retry({ attempts, delaySeconds }, () => ctx.http.expectOk(url, opts.headers ? { headers: opts.headers } : undefined));
33
- ctx.log("health OK", { url: url.split("?")[0]! });
31
+ ctx.log('checking health', { url: url.split('?')[0]!, attempts });
32
+ await ctx.retry({ attempts, delaySeconds }, () =>
33
+ ctx.http.expectOk(url, opts.headers ? { headers: opts.headers } : undefined),
34
+ );
35
+ ctx.log('health OK', { url: url.split('?')[0]! });
34
36
  }