redkite 0.1.7 → 0.1.9

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 (123) hide show
  1. package/README.md +343 -26
  2. package/dist/build.d.ts +2 -0
  3. package/dist/build.d.ts.map +1 -1
  4. package/dist/build.js +11 -1
  5. package/dist/build.js.map +1 -1
  6. package/dist/checks.d.ts.map +1 -1
  7. package/dist/checks.js +4 -0
  8. package/dist/checks.js.map +1 -1
  9. package/dist/cli/agent.d.ts +2 -0
  10. package/dist/cli/agent.d.ts.map +1 -1
  11. package/dist/cli/agent.js +23 -1
  12. package/dist/cli/agent.js.map +1 -1
  13. package/dist/cli/config.d.ts.map +1 -1
  14. package/dist/cli/config.js +15 -2
  15. package/dist/cli/config.js.map +1 -1
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli/index.js +98 -30
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +20 -1
  21. package/dist/config.js.map +1 -1
  22. package/dist/deploy.d.ts +4 -1
  23. package/dist/deploy.d.ts.map +1 -1
  24. package/dist/deploy.js +77 -23
  25. package/dist/deploy.js.map +1 -1
  26. package/dist/dockerfile.js +6 -7
  27. package/dist/dockerfile.js.map +1 -1
  28. package/dist/environment.d.ts +5 -0
  29. package/dist/environment.d.ts.map +1 -0
  30. package/dist/environment.js +20 -0
  31. package/dist/environment.js.map +1 -0
  32. package/dist/host.d.ts +2 -0
  33. package/dist/host.d.ts.map +1 -1
  34. package/dist/host.js +6 -0
  35. package/dist/host.js.map +1 -1
  36. package/dist/index.d.ts +4 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +4 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/localHost.d.ts.map +1 -1
  41. package/dist/localHost.js +2 -0
  42. package/dist/localHost.js.map +1 -1
  43. package/dist/plugin.d.ts +15 -0
  44. package/dist/plugin.d.ts.map +1 -0
  45. package/dist/plugin.js +21 -0
  46. package/dist/plugin.js.map +1 -0
  47. package/dist/plugins/bitwarden.d.ts +11 -0
  48. package/dist/plugins/bitwarden.d.ts.map +1 -0
  49. package/dist/plugins/bitwarden.js +40 -0
  50. package/dist/plugins/bitwarden.js.map +1 -0
  51. package/dist/plugins/digitalOcean.d.ts +19 -0
  52. package/dist/plugins/digitalOcean.d.ts.map +1 -0
  53. package/dist/plugins/digitalOcean.js +71 -0
  54. package/dist/plugins/digitalOcean.js.map +1 -0
  55. package/dist/plugins/index.d.ts +5 -0
  56. package/dist/plugins/index.d.ts.map +1 -0
  57. package/dist/plugins/index.js +5 -0
  58. package/dist/plugins/index.js.map +1 -0
  59. package/dist/plugins/rds.d.ts +14 -0
  60. package/dist/plugins/rds.d.ts.map +1 -0
  61. package/dist/plugins/rds.js +86 -0
  62. package/dist/plugins/rds.js.map +1 -0
  63. package/dist/plugins/snapshot.d.ts +7 -0
  64. package/dist/plugins/snapshot.d.ts.map +1 -0
  65. package/dist/plugins/snapshot.js +29 -0
  66. package/dist/plugins/snapshot.js.map +1 -0
  67. package/dist/presets/nodeApp.d.ts +1 -2
  68. package/dist/presets/nodeApp.d.ts.map +1 -1
  69. package/dist/presets/nodeApp.js +0 -1
  70. package/dist/presets/nodeApp.js.map +1 -1
  71. package/dist/recover.d.ts +19 -0
  72. package/dist/recover.d.ts.map +1 -0
  73. package/dist/recover.js +58 -0
  74. package/dist/recover.js.map +1 -0
  75. package/dist/secrets/store.d.ts +5 -2
  76. package/dist/secrets/store.d.ts.map +1 -1
  77. package/dist/secrets/store.js +25 -14
  78. package/dist/secrets/store.js.map +1 -1
  79. package/dist/source.d.ts +6 -1
  80. package/dist/source.d.ts.map +1 -1
  81. package/dist/source.js +12 -4
  82. package/dist/source.js.map +1 -1
  83. package/dist/sshHost.d.ts +2 -0
  84. package/dist/sshHost.d.ts.map +1 -1
  85. package/dist/sshHost.js +29 -11
  86. package/dist/sshHost.js.map +1 -1
  87. package/dist/steps.d.ts +0 -7
  88. package/dist/steps.d.ts.map +1 -1
  89. package/dist/steps.js +6 -3
  90. package/dist/steps.js.map +1 -1
  91. package/dist/types.d.ts +7 -5
  92. package/dist/types.d.ts.map +1 -1
  93. package/package.json +1 -1
  94. package/src/build.ts +12 -1
  95. package/src/checks.ts +4 -0
  96. package/src/cli/agent.ts +28 -1
  97. package/src/cli/config.ts +19 -2
  98. package/src/cli/index.ts +118 -29
  99. package/src/config.ts +27 -1
  100. package/src/deploy.ts +101 -29
  101. package/src/dockerfile.ts +7 -11
  102. package/src/environment.ts +24 -0
  103. package/src/host.ts +11 -0
  104. package/src/index.ts +4 -1
  105. package/src/localHost.ts +3 -0
  106. package/src/plugin.ts +50 -0
  107. package/src/plugins/bitwarden.ts +57 -0
  108. package/src/plugins/digitalOcean.ts +111 -0
  109. package/src/plugins/index.ts +4 -0
  110. package/src/plugins/rds.ts +126 -0
  111. package/src/plugins/snapshot.ts +44 -0
  112. package/src/presets/nodeApp.ts +1 -3
  113. package/src/recover.ts +90 -0
  114. package/src/secrets/store.ts +39 -20
  115. package/src/source.ts +20 -5
  116. package/src/sshHost.ts +35 -11
  117. package/src/steps.ts +7 -6
  118. package/src/types.ts +29 -12
  119. package/dist/secrets/bitwarden.d.ts +0 -3
  120. package/dist/secrets/bitwarden.d.ts.map +0 -1
  121. package/dist/secrets/bitwarden.js +0 -6
  122. package/dist/secrets/bitwarden.js.map +0 -1
  123. package/src/secrets/bitwarden.ts +0 -7
@@ -0,0 +1,44 @@
1
+ import type { Plan } from "../pipeline.js";
2
+
3
+ // What the two snapshot plugins share. A snapshot is a control-plane call
4
+ // rather than anything the deploy host does, so it runs where the credentials
5
+ // are, which is the process running redkite.
6
+
7
+ // Hung before the swap and before any migration, so the thing being protected
8
+ // from is still ahead of it. Steps run in the order the config lists them, so a
9
+ // snapshot has to be written above the migrate it guards
10
+ export type SnapshotPoint = `swap:before:${string}`;
11
+
12
+ // Letters, digits and single hyphens, which is the intersection of what RDS
13
+ // accepts as a snapshot identifier and what a point may be named
14
+ export function slug(value: string) {
15
+ return value
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, "-")
18
+ .replace(/^-+|-+$/g, "")
19
+ .replace(/-{2,}/g, "-");
20
+ }
21
+
22
+ // Sortable, second-resolution, and legal in every identifier below. Two
23
+ // snapshots in one second would collide, and two deploys in one second is not
24
+ // a thing a swap can do
25
+ export function stamp(now: Date) {
26
+ return now.toISOString().replace(/[-:T]/g, "").slice(0, 14);
27
+ }
28
+
29
+ // Names the deploy it was taken for. A snapshot nobody can place is one nobody
30
+ // will dare restore
31
+ export function snapshotName(prefix: string, environment: string, now: Date) {
32
+ return slug(`${prefix}-${environment}-${stamp(now)}`);
33
+ }
34
+
35
+ // Read before the run starts, so a missing token is a config that fails while
36
+ // the host is untouched rather than a deploy that stops with the swap ahead
37
+ export function tokenFrom(name: string, plan: Plan) {
38
+ const value = process.env[name];
39
+ if (value) return value;
40
+
41
+ throw new Error(
42
+ `${name} is not set, and ${plan.config.project} snapshots its database before swapping`,
43
+ );
44
+ }
@@ -1,4 +1,4 @@
1
- import type { BuildSpec, CarryPath, SourcemapSpec } from "../types.js";
1
+ import type { BuildSpec, CarryPath } from "../types.js";
2
2
 
3
3
  type NodeAppOptions = {
4
4
  builder?: string;
@@ -12,7 +12,6 @@ type NodeAppOptions = {
12
12
  carry?: CarryPath[];
13
13
  submodules?: boolean;
14
14
  caches?: string[];
15
- sourcemaps?: SourcemapSpec;
16
15
  };
17
16
 
18
17
  export function nodeApp(options: NodeAppOptions): BuildSpec {
@@ -41,6 +40,5 @@ export function nodeApp(options: NodeAppOptions): BuildSpec {
41
40
  aptPackages: ["git"],
42
41
  runtimePackages: ["curl"],
43
42
  runtimeSteps: [],
44
- sourcemaps: options.sourcemaps,
45
43
  };
46
44
  }
package/src/recover.ts ADDED
@@ -0,0 +1,90 @@
1
+ import { revert } from "./deploy.js";
2
+ import { Docker } from "./docker.js";
3
+ import { finalHost, type Host } from "./host.js";
4
+ import { silent, type Log } from "./log.js";
5
+ import { topologyFor, type Topology } from "./topology.js";
6
+ import type { Deployment } from "./types.js";
7
+
8
+ // Undoing a run that did not finish, from a process that is not it. A cancelled
9
+ // job is killed rather than asked, so nothing it was holding survives to tidy
10
+ // up: what the host is left in has to be readable from the host alone.
11
+
12
+ export type RecoverOptions = {
13
+ config: Deployment;
14
+ environment: string;
15
+ host: Host;
16
+ log?: Log;
17
+ };
18
+
19
+ export type RolledBack = {
20
+ // Apps put back on the live address they were serving from
21
+ restored: string[];
22
+ // Apps that were already where they belong, which is most runs
23
+ untouched: string[];
24
+ };
25
+
26
+ // A retired container is the whole signal. One exists between the swap and the
27
+ // cleanup that removes it, so finding one means a run moved an address and did
28
+ // not get to say whether that worked
29
+ export async function rollback(options: RecoverOptions): Promise<RolledBack> {
30
+ const { log, topology, docker } = opened(options);
31
+
32
+ const restored: string[] = [];
33
+ const untouched: string[] = [];
34
+
35
+ for (const app of topology.apps) {
36
+ if (!(await docker.container.exists(app.retired))) {
37
+ untouched.push(app.container);
38
+ continue;
39
+ }
40
+
41
+ log(`Putting ${app.container} back`);
42
+ await revert(docker, topology, app);
43
+ restored.push(app.container);
44
+ }
45
+
46
+ return { restored, untouched };
47
+ }
48
+
49
+ export type TakenDown = {
50
+ stopped: string[];
51
+ };
52
+
53
+ // Stopped rather than removed, so the next run adopts them where it left off.
54
+ // For an environment that exists to be built against rather than served from,
55
+ // which is the one a cancelled job should not leave running
56
+ export async function down(options: RecoverOptions): Promise<TakenDown> {
57
+ const { log, topology, docker } = opened(options);
58
+ const stopped: string[] = [];
59
+
60
+ for (const name of every(topology)) {
61
+ if (!(await docker.container.isRunning(name))) continue;
62
+
63
+ log(`Stopping ${name}`);
64
+ await docker.container.stop(name);
65
+ stopped.push(name);
66
+ }
67
+
68
+ return { stopped };
69
+ }
70
+
71
+ // Everything this environment named, whether or not it was ever created. The
72
+ // proxy is derived rather than listed, so it would be missed by walking the
73
+ // config alone
74
+ function every(topology: Topology) {
75
+ return [
76
+ ...topology.apps.flatMap((app) => [app.container, app.retired, app.failed]),
77
+ ...topology.services.map((service) => service.container),
78
+ topology.router.container,
79
+ ];
80
+ }
81
+
82
+ // Both of these exist because something was stopped, so neither may be refused
83
+ // by that same stop
84
+ function opened(options: RecoverOptions) {
85
+ return {
86
+ log: options.log ?? silent,
87
+ topology: topologyFor(options.config, options.environment),
88
+ docker: new Docker(finalHost(options.host)),
89
+ };
90
+ }
@@ -16,12 +16,11 @@ import type { SecretStore } from "./refs.js";
16
16
 
17
17
  const run = promisify(execFile);
18
18
 
19
+ // A session the caller already holds, or what it takes to obtain one. Both
20
+ // spellings in one object would be a pair of credentials nobody can tell apart
19
21
  export type BitwardenCredentials = {
20
- clientId: string;
21
- clientSecret: string;
22
- password: string;
23
22
  detail?: (message: string) => void;
24
- };
23
+ } & ({ session: string } | { clientId: string; clientSecret: string; password: string });
25
24
 
26
25
  // Used when bw is not already on PATH. Pinned, because an unpinned CLI is a
27
26
  // different program on a machine that has never run a deploy before
@@ -53,22 +52,7 @@ export async function bitwardenStore(
53
52
  maxBuffer: 32 * 1024 * 1024,
54
53
  });
55
54
 
56
- detail("unlocking the vault");
57
-
58
- // Already logged in is not an error, the session is what matters
59
- await bw(["login", "--apikey"], {
60
- BW_CLIENTID: credentials.clientId,
61
- BW_CLIENTSECRET: credentials.clientSecret,
62
- }).catch(() => undefined);
63
-
64
- const unlocked = await bw(["unlock", "--passwordenv", "BW_PASSWORD", "--raw"], {
65
- BW_PASSWORD: credentials.password,
66
- }).catch((error: unknown) => {
67
- throw new Error(`Could not unlock the Bitwarden vault: ${messageOf(error)}`);
68
- });
69
-
70
- const session = unlocked.stdout.trim();
71
- if (!session) throw new Error("Bitwarden unlocked without a session");
55
+ const session = await unlock(bw, credentials, detail);
72
56
 
73
57
  // An item added since the last deploy is not in the local vault otherwise,
74
58
  // and bw get answers "not found" rather than fetching it
@@ -97,6 +81,41 @@ export async function bitwardenStore(
97
81
  };
98
82
  }
99
83
 
84
+ type Bw = (args: string[], env?: Record<string, string>) => Promise<{ stdout: string }>;
85
+
86
+ // A session handed in is one nobody had to obtain, which is the whole of what
87
+ // a key in the environment buys: no api credentials, no master password, and
88
+ // two fewer round trips before the first item is read
89
+ async function unlock(
90
+ bw: Bw,
91
+ credentials: BitwardenCredentials,
92
+ detail: (message: string) => void,
93
+ ) {
94
+ if ("session" in credentials) {
95
+ detail("using the session it was given");
96
+ return credentials.session;
97
+ }
98
+
99
+ detail("unlocking the vault");
100
+
101
+ // Already logged in is not an error, the session is what matters
102
+ await bw(["login", "--apikey"], {
103
+ BW_CLIENTID: credentials.clientId,
104
+ BW_CLIENTSECRET: credentials.clientSecret,
105
+ }).catch(() => undefined);
106
+
107
+ const unlocked = await bw(["unlock", "--passwordenv", "BW_PASSWORD", "--raw"], {
108
+ BW_PASSWORD: credentials.password,
109
+ }).catch((error: unknown) => {
110
+ throw new Error(`Could not unlock the Bitwarden vault: ${messageOf(error)}`);
111
+ });
112
+
113
+ const session = unlocked.stdout.trim();
114
+ if (session) return session;
115
+
116
+ throw new Error("Bitwarden unlocked without a session");
117
+ }
118
+
100
119
  type Cli = { file: string; prefix: string[] };
101
120
 
102
121
  // A machine with the CLI installed pays nothing. One without it installs the
package/src/source.ts CHANGED
@@ -13,6 +13,12 @@ export type Source = {
13
13
  tree: string;
14
14
  };
15
15
 
16
+ // What a clone is resolved against. A branch is tracked and a tag or a commit
17
+ // is pinned, and which of the three it is decides where git is asked to look
18
+ export type RefKind = "branch" | "tag" | "commit";
19
+
20
+ export type Ref = { kind: RefKind; name: string };
21
+
16
22
  export type SourceRequest = {
17
23
  // Names the mirror and the checkout. Keyed per app and environment rather
18
24
  // than per repository, so two deploys never fetch into one directory at once
@@ -23,11 +29,19 @@ export type SourceRequest = {
23
29
  // What of a path goes into the build. Absent leaves it to the work tree's
24
30
  // own .gitignore, which is the only other thing that can say
25
31
  include?: string[];
26
- branch: string;
32
+ ref: Ref;
27
33
  submodules: boolean;
28
34
  detail?: (message: string) => void;
29
35
  };
30
36
 
37
+ // Where each kind lives. A tag is peeled, so an annotated one answers with the
38
+ // commit it points at rather than with the tag object
39
+ const REVISION: Record<RefKind, (name: string) => string> = {
40
+ branch: (name) => `refs/heads/${name}`,
41
+ tag: (name) => `refs/tags/${name}^{commit}`,
42
+ commit: (name) => `${name}^{commit}`,
43
+ };
44
+
31
45
  // accept-new rather than no: a host key that changes is still worth refusing
32
46
  const GIT_SSH = "ssh -o StrictHostKeyChecking=accept-new";
33
47
 
@@ -110,8 +124,9 @@ async function clonedSource(
110
124
  const detail = request.detail ?? (() => {});
111
125
  const mirror = `${host.cache}/mirrors/${request.name}.git`;
112
126
  const path = `${host.cache}/source/${request.name}`;
127
+ const { kind, name } = request.ref;
113
128
 
114
- detail(`fetching ${request.branch}`);
129
+ detail(`fetching ${name}`);
115
130
  await run(host, `updating the mirror of ${repo}`, [
116
131
  `if [ -d '${mirror}' ]; then`,
117
132
  ` git -C '${mirror}' remote set-url origin '${repo}'`,
@@ -122,13 +137,13 @@ async function clonedSource(
122
137
  "fi",
123
138
  ]);
124
139
 
125
- const resolved = await run(host, `resolving ${request.branch}`, [
126
- `git -C '${mirror}' rev-parse 'refs/heads/${request.branch}'`,
140
+ const resolved = await run(host, `resolving ${name}`, [
141
+ `git -C '${mirror}' rev-parse '${REVISION[kind](name)}'`,
127
142
  ]);
128
143
 
129
144
  const release = resolved.stdout.trim();
130
145
  if (!/^[0-9a-f]{40}$/.test(release)) {
131
- throw new Error(`${repo} has no branch ${request.branch}`);
146
+ throw new Error(`${repo} has no ${kind} ${name}`);
132
147
  }
133
148
 
134
149
  detail(`checking out ${release.slice(0, 7)}`);
package/src/sshHost.ts CHANGED
@@ -3,6 +3,7 @@ import { dirname } from "node:path";
3
3
 
4
4
  import type { Host, OnLine, Result } from "./host.js";
5
5
  import { quote, spawnCollect } from "./shell.js";
6
+ import type { HostKeys } from "./types.js";
6
7
 
7
8
  // The deploy host, reached over ssh. The agent is forwarded rather than a key,
8
9
  // there is nothing to forward, and the agent travels with the connection so the
@@ -23,23 +24,40 @@ export type SshOptions = {
23
24
  shell?: Shell;
24
25
  directory?: string;
25
26
  cache?: string;
27
+ // How the host's key is checked. Defaults to accept-new
28
+ hostKeys?: HostKeys;
26
29
  // Aborting kills the ssh client. The command it was carrying keeps running on
27
30
  // the other machine, which is what stop is for
28
31
  signal?: AbortSignal;
29
32
  };
30
33
 
34
+ // What ssh calls each of them. accept-new is the default rather than no: a
35
+ // deploy hands over a vault's contents, and the last thing it should do that
36
+ // to is a machine that answered to the address and nothing else
37
+ const CHECKING: Record<HostKeys, string> = {
38
+ "accept-new": "accept-new",
39
+ strict: "yes",
40
+ off: "no",
41
+ };
42
+
31
43
  // One TCP connection and one authentication for the whole deploy. Without it
32
44
  // every command pays a handshake, which is most of what a command costs
33
- const MULTIPLEX = [
34
- "-o",
35
- "ControlMaster=auto",
36
- "-o",
37
- "ControlPersist=60s",
38
- "-o",
39
- "StrictHostKeyChecking=no",
40
- // The host clones from GitHub on our behalf rather than us shipping a tree
41
- "-A",
42
- ];
45
+ function multiplex(hostKeys: HostKeys = "accept-new") {
46
+ return [
47
+ "-o",
48
+ "ControlMaster=auto",
49
+ "-o",
50
+ "ControlPersist=60s",
51
+ "-o",
52
+ `StrictHostKeyChecking=${CHECKING[hostKeys]}`,
53
+ // Never a prompt. Unknown under strict is a refusal with a reason, and a
54
+ // question nobody is there to answer is a deploy that hangs
55
+ "-o",
56
+ "BatchMode=yes",
57
+ // The host clones from GitHub on our behalf rather than us shipping a tree
58
+ "-A",
59
+ ];
60
+ }
43
61
 
44
62
  export async function sshHost(bastion: string, options: SshOptions = {}): Promise<Host> {
45
63
  const control = `/tmp/redkite-${randomUUID().slice(0, 8)}.control`;
@@ -57,8 +75,10 @@ export async function sshHost(bastion: string, options: SshOptions = {}): Promis
57
75
  options.shell ??
58
76
  ((command: string) => spawnCollect("sh", ["-c", command], { signal }));
59
77
 
78
+ const flags = multiplex(options.hostKeys);
79
+
60
80
  const argv = (command: string) => [
61
- ...MULTIPLEX,
81
+ ...flags,
62
82
  "-o",
63
83
  `ControlPath=${control}`,
64
84
  bastion,
@@ -111,6 +131,10 @@ export async function sshHost(bastion: string, options: SshOptions = {}): Promis
111
131
  pipe: async (local, remote) =>
112
132
  await shell(`${local} | ssh ${argv(remote).map(quote).join(" ")}`),
113
133
 
134
+ // Runs on the connection that is already open, and without the signal that
135
+ // killed whatever made it necessary
136
+ final: async (command) => await final(argv(command)),
137
+
114
138
  // Signalled where they are running, not here. Killing the client would
115
139
  // leave the build going with nothing left able to reach it
116
140
  stop: async (name) => {
package/src/steps.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { environmentOf } from "./config.js";
2
+ import { envFlags } from "./environment.js";
2
3
  import type { Built, Plan, Step } from "./pipeline.js";
3
4
  import type { Topology } from "./topology.js";
4
5
  import type { AppSpec, StepNetwork } from "./types.js";
@@ -48,10 +49,16 @@ export function migrate(options: MigrateOptions): Step<`swap:before:${string}`>
48
49
  const image = builderOf(input, options.app);
49
50
  context.task.detail(`${options.app}: ${options.command}`);
50
51
 
52
+ // Nothing from the vault is in the image, so a migration reads its
53
+ // database url from the environment it is given rather than from a file
54
+ // the build left behind
55
+ const app = context.config.apps.find((item) => item.name === options.app);
56
+
51
57
  await context.docker.runOrThrow(
52
58
  [
53
59
  "run --rm",
54
60
  ...attachment(options.network ?? "host", context.topology),
61
+ ...(app ? await envFlags(app, context) : []),
55
62
  "--workdir /app",
56
63
  image,
57
64
  ...command,
@@ -90,12 +97,6 @@ export function builderOf(input: Built, name: string) {
90
97
  throw new Error(`${name} has no builder image, so nothing can be run in it`);
91
98
  }
92
99
 
93
- type SentryOptions = { stripFromImage: string };
94
-
95
- export function sentry(options: SentryOptions) {
96
- return { provider: "sentry" as const, stripFromImage: options.stripFromImage };
97
- }
98
-
99
100
  export function routeOf(app: AppSpec) {
100
101
  return app.route;
101
102
  }
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { AnyStep } from "./pipeline.js";
2
+ import type { Plugin } from "./plugin.js";
2
3
 
3
4
  // The shapes a deploy config is written against. Nothing here knows about
4
5
  // Docker or git, so a config can be planned and asserted on without a host to
@@ -33,11 +34,26 @@ export type Environment = {
33
34
  // the alias the apps know it by
34
35
  export type StepNetwork = "host" | "deployment" | "none" | { named: string };
35
36
 
37
+ // How the deploy host proves it is the machine it says it is, before a key,
38
+ // an environment file or an image is handed to it
39
+ export type HostKeys =
40
+ // Trusted the first time and refused if the key ever changes after that,
41
+ // which is what a host on a stable address wants and needs no setup
42
+ | "accept-new"
43
+ // Refused unless it is already in known_hosts. Nothing is taken on trust,
44
+ // at the cost of putting the key there before the first deploy
45
+ | "strict"
46
+ // Checked not at all. For a host whose address is handed out again and
47
+ // again, and only where nothing on the way to it can be listened to
48
+ | "off";
49
+
36
50
  export type DeployHost = {
37
51
  // SSH destination, user@address
38
52
  bastion: string;
39
53
  // Socket path on that machine. Local when absent, deploying to this one
40
54
  socket?: string;
55
+ // Defaults to accept-new
56
+ hostKeys?: HostKeys;
41
57
  };
42
58
 
43
59
  // One item in a store, named at the point of use rather than through a lookup
@@ -127,16 +143,6 @@ export type BuildSpec = {
127
143
  // Shell commands run in the runtime image, for what a package manager cannot
128
144
  // install. Above the output copy, so a new commit does not repeat them
129
145
  runtimeSteps: string[];
130
- // Uploads source maps during the build, then deletes them from the image
131
- sourcemaps?: SourcemapSpec;
132
- };
133
-
134
- export type SourcemapSpec = {
135
- // Which service receives the maps, "sentry" is the only one today
136
- provider: "sentry";
137
- // Env var stripped from the shipped file, it is a build-time credential and
138
- // has no reason to travel inside the image
139
- stripFromImage: string;
140
146
  };
141
147
 
142
148
  // How a build is decided to work. The commands run in the builder image, which
@@ -193,6 +199,13 @@ export type AppSpec = {
193
199
  // Environment for the app. Several are merged in the order written, so a
194
200
  // shared ref can come first and a per-app one override it
195
201
  secrets?: SecretRefs;
202
+ // What this app is built from, overriding the environment's branch. At most
203
+ // one of the three. A branch is tracked, so every deploy takes whatever its
204
+ // head is by then; a tag or a commit is pinned, and the app stops moving
205
+ // until this line does. Ignored by an app built from a path
206
+ branch?: string;
207
+ tag?: string;
208
+ commit?: string;
196
209
  // Where the app sits in the repository, for a monorepo whose root is not it.
197
210
  // Build steps and the shipped command run there. The dependency install does
198
211
  // not: a workspace lockfile is resolved at the root for every package at once
@@ -237,7 +250,11 @@ export type Deployment = {
237
250
  // The applications. Everything derived, addresses, container names, nginx
238
251
  // upstreams and location blocks, is a function of this list
239
252
  apps: AppSpec[];
240
- // Work injected into the run, from this file or from a plugin that answers
241
- // with some. Addressed by where it runs, so nothing here has to be called
253
+ // Work injected into the run. Addressed by where it runs, so nothing here
254
+ // has to be called
242
255
  steps?: AnyStep[];
256
+ // Everything this deployment opts into: the vault that answers its secret
257
+ // refs, and whatever else brings steps of its own. Nothing a plugin carries
258
+ // happens until it is listed here, redkite's own vault included
259
+ plugins?: Plugin[];
243
260
  };
@@ -1,3 +0,0 @@
1
- import type { SecretRef } from "../types.js";
2
- export declare function bitwarden(id: string): SecretRef;
3
- //# sourceMappingURL=bitwarden.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bitwarden.d.ts","sourceRoot":"","sources":["../../src/secrets/bitwarden.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAE/C"}
@@ -1,6 +0,0 @@
1
- // Names an item in a Bitwarden vault. The id is not a secret, it is a pointer,
2
- // so it belongs in the config while the credentials arrive at deploy time
3
- export function bitwarden(id) {
4
- return { provider: "bitwarden", id };
5
- }
6
- //# sourceMappingURL=bitwarden.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bitwarden.js","sourceRoot":"","sources":["../../src/secrets/bitwarden.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,0EAA0E;AAC1E,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,EAAE,CAAC;AACvC,CAAC"}
@@ -1,7 +0,0 @@
1
- import type { SecretRef } from "../types.js";
2
-
3
- // Names an item in a Bitwarden vault. The id is not a secret, it is a pointer,
4
- // so it belongs in the config while the credentials arrive at deploy time
5
- export function bitwarden(id: string): SecretRef {
6
- return { provider: "bitwarden", id };
7
- }