redkite 0.1.6
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/LICENSE +21 -0
- package/README.md +819 -0
- package/bin/redkite.mjs +12 -0
- package/dist/build.d.ts +27 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +132 -0
- package/dist/build.js.map +1 -0
- package/dist/checks.d.ts +4 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +49 -0
- package/dist/checks.js.map +1 -0
- package/dist/cli/agent.d.ts +2 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +24 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +217 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +438 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/log.d.ts +9 -0
- package/dist/cli/log.d.ts.map +1 -0
- package/dist/cli/log.js +128 -0
- package/dist/cli/log.js.map +1 -0
- package/dist/cli/screen.d.ts +34 -0
- package/dist/cli/screen.d.ts.map +1 -0
- package/dist/cli/screen.js +298 -0
- package/dist/cli/screen.js.map +1 -0
- package/dist/cli/viewer.d.ts +9 -0
- package/dist/cli/viewer.d.ts.map +1 -0
- package/dist/cli/viewer.js +139 -0
- package/dist/cli/viewer.js.map +1 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +79 -0
- package/dist/config.js.map +1 -0
- package/dist/deploy.d.ts +19 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +275 -0
- package/dist/deploy.js.map +1 -0
- package/dist/docker.d.ts +96 -0
- package/dist/docker.d.ts.map +1 -0
- package/dist/docker.js +325 -0
- package/dist/docker.js.map +1 -0
- package/dist/dockerfile.d.ts +14 -0
- package/dist/dockerfile.d.ts.map +1 -0
- package/dist/dockerfile.js +160 -0
- package/dist/dockerfile.js.map +1 -0
- package/dist/health.d.ts +13 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +54 -0
- package/dist/health.js.map +1 -0
- package/dist/host.d.ts +21 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +27 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +5 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +45 -0
- package/dist/layout.js.map +1 -0
- package/dist/localHost.d.ts +7 -0
- package/dist/localHost.d.ts.map +1 -0
- package/dist/localHost.js +32 -0
- package/dist/localHost.js.map +1 -0
- package/dist/log.d.ts +14 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +15 -0
- package/dist/log.js.map +1 -0
- package/dist/nginx.d.ts +4 -0
- package/dist/nginx.d.ts.map +1 -0
- package/dist/nginx.js +53 -0
- package/dist/nginx.js.map +1 -0
- package/dist/pipeline.d.ts +103 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +142 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/presets/index.d.ts +3 -0
- package/dist/presets/index.d.ts.map +1 -0
- package/dist/presets/index.js +3 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/presets/nextApp.d.ts +15 -0
- package/dist/presets/nextApp.d.ts.map +1 -0
- package/dist/presets/nextApp.js +53 -0
- package/dist/presets/nextApp.js.map +1 -0
- package/dist/presets/nodeApp.d.ts +19 -0
- package/dist/presets/nodeApp.d.ts.map +1 -0
- package/dist/presets/nodeApp.js +29 -0
- package/dist/presets/nodeApp.js.map +1 -0
- package/dist/secrets/bitwarden.d.ts +3 -0
- package/dist/secrets/bitwarden.d.ts.map +1 -0
- package/dist/secrets/bitwarden.js +6 -0
- package/dist/secrets/bitwarden.js.map +1 -0
- package/dist/secrets/refs.d.ts +9 -0
- package/dist/secrets/refs.d.ts.map +1 -0
- package/dist/secrets/refs.js +20 -0
- package/dist/secrets/refs.js.map +1 -0
- package/dist/secrets/store.d.ts +9 -0
- package/dist/secrets/store.d.ts.map +1 -0
- package/dist/secrets/store.js +104 -0
- package/dist/secrets/store.js.map +1 -0
- package/dist/services/ensure.d.ts +18 -0
- package/dist/services/ensure.d.ts.map +1 -0
- package/dist/services/ensure.js +93 -0
- package/dist/services/ensure.js.map +1 -0
- package/dist/services/index.d.ts +19 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +27 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/planned.d.ts +20 -0
- package/dist/services/planned.d.ts.map +1 -0
- package/dist/services/planned.js +77 -0
- package/dist/services/planned.js.map +1 -0
- package/dist/shell.d.ts +11 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +65 -0
- package/dist/shell.js.map +1 -0
- package/dist/source.d.ts +16 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +108 -0
- package/dist/source.js.map +1 -0
- package/dist/sshHost.d.ts +15 -0
- package/dist/sshHost.d.ts.map +1 -0
- package/dist/sshHost.js +91 -0
- package/dist/sshHost.js.map +1 -0
- package/dist/steps.d.ts +27 -0
- package/dist/steps.d.ts.map +1 -0
- package/dist/steps.js +69 -0
- package/dist/steps.js.map +1 -0
- package/dist/topology.d.ts +40 -0
- package/dist/topology.d.ts.map +1 -0
- package/dist/topology.js +113 -0
- package/dist/topology.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +64 -0
- package/src/build.ts +236 -0
- package/src/checks.ts +65 -0
- package/src/cli/agent.ts +28 -0
- package/src/cli/config.ts +275 -0
- package/src/cli/index.ts +559 -0
- package/src/cli/log.ts +168 -0
- package/src/cli/screen.ts +409 -0
- package/src/cli/viewer.ts +192 -0
- package/src/config.ts +90 -0
- package/src/deploy.ts +409 -0
- package/src/docker.ts +433 -0
- package/src/dockerfile.ts +228 -0
- package/src/health.ts +83 -0
- package/src/host.ts +54 -0
- package/src/index.ts +25 -0
- package/src/layout.ts +41 -0
- package/src/localHost.ts +50 -0
- package/src/log.ts +38 -0
- package/src/nginx.ts +63 -0
- package/src/pipeline.ts +335 -0
- package/src/presets/index.ts +2 -0
- package/src/presets/nextApp.ts +70 -0
- package/src/presets/nodeApp.ts +46 -0
- package/src/secrets/bitwarden.ts +7 -0
- package/src/secrets/refs.ts +32 -0
- package/src/secrets/store.ts +147 -0
- package/src/services/ensure.ts +160 -0
- package/src/services/index.ts +48 -0
- package/src/services/planned.ts +119 -0
- package/src/shell.ts +95 -0
- package/src/source.ts +171 -0
- package/src/sshHost.ts +145 -0
- package/src/steps.ts +101 -0
- package/src/topology.ts +199 -0
- package/src/types.ts +243 -0
package/README.md
ADDED
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="media/redkite.png" alt="redkite" width="200">
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/redkite-cd"><img alt="npm version" src="https://img.shields.io/npm/v/redkite-cd.svg"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/redkite-cd"><img alt="npm downloads" src="https://img.shields.io/npm/dm/redkite-cd.svg"></a>
|
|
8
|
+
<a href="https://nodejs.org"><img alt="node" src="https://img.shields.io/node/v/redkite-cd.svg"></a>
|
|
9
|
+
<a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/redkite-cd.svg"></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
Blue-green Docker deployment from one config file, over ssh.<br>
|
|
14
|
+
No engine, no agent, no dependencies.
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
Describe a deployment once. Redkite derives every container name, IP address,
|
|
18
|
+
volume, cache key, nginx upstream and location block from it, then builds your
|
|
19
|
+
images on the machine that runs them and swaps traffic over with a health check
|
|
20
|
+
and an automatic revert.
|
|
21
|
+
|
|
22
|
+
A deploy is git, a Dockerfile and the docker CLI, driven over one ssh
|
|
23
|
+
connection. There is nothing to install on the server and nothing running
|
|
24
|
+
between deploys.
|
|
25
|
+
|
|
26
|
+
## Contents
|
|
27
|
+
|
|
28
|
+
- [Quick start](#quick-start)
|
|
29
|
+
- [Features](#features)
|
|
30
|
+
- [Requirements](#requirements)
|
|
31
|
+
- [Configuration](#configuration)
|
|
32
|
+
- [Hooks](#hooks)
|
|
33
|
+
- [Verifying a build](#verifying-a-build)
|
|
34
|
+
- [CLI](#cli)
|
|
35
|
+
- [How a deploy runs](#how-a-deploy-runs)
|
|
36
|
+
- [Design](#design)
|
|
37
|
+
- [Contributing](#contributing)
|
|
38
|
+
- [License](#license)
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
npm install redkite-cd
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Add `redkite.config.ts` at the root of your project:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { defineDeployment, nextApp, nodeApp, redis } from "redkite-cd";
|
|
50
|
+
|
|
51
|
+
export default defineDeployment({
|
|
52
|
+
project: "acme",
|
|
53
|
+
|
|
54
|
+
services: [redis()],
|
|
55
|
+
|
|
56
|
+
apps: [
|
|
57
|
+
{
|
|
58
|
+
name: "web",
|
|
59
|
+
repo: "git@github.com:acme/web.git",
|
|
60
|
+
route: "/",
|
|
61
|
+
port: 3000,
|
|
62
|
+
build: nextApp(),
|
|
63
|
+
health: { path: "/api/health", expect: (body) => body.status === "ok" },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "api",
|
|
67
|
+
repo: "git@github.com:acme/api.git",
|
|
68
|
+
route: "/api/",
|
|
69
|
+
port: 3001,
|
|
70
|
+
build: nodeApp({
|
|
71
|
+
steps: ["yarn build"],
|
|
72
|
+
output: "/app/dist",
|
|
73
|
+
entrypoint: ["node", "/app/index.js"],
|
|
74
|
+
}),
|
|
75
|
+
health: { path: "/health", expect: (body) => body.status === "up" },
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
npx redkite plan production # what it will do, no host needed
|
|
85
|
+
npx redkite deploy production # build, swap, health check, revert on failure
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Nothing in that config names an IP address, a container, a network, a cache key,
|
|
89
|
+
a `retired-` prefix, or an nginx directive. Adding a third app is six lines.
|
|
90
|
+
|
|
91
|
+
## Features
|
|
92
|
+
|
|
93
|
+
- **Blue-green by default.** The running container moves to a retired address
|
|
94
|
+
without being stopped, so it keeps answering while the new one starts. One
|
|
95
|
+
unhealthy app reverts all of them.
|
|
96
|
+
- **Derived topology.** Container names, addresses, volumes, cache keys, nginx
|
|
97
|
+
upstreams and location blocks are functions of the app list, so two of them
|
|
98
|
+
cannot collide and adding an app cannot renumber another.
|
|
99
|
+
- **Builds on the deploy host.** The image is created inside the daemon that
|
|
100
|
+
will run it, so there is no export, no tarball and no transfer.
|
|
101
|
+
- **Nothing crosses the wire.** The host clones your repositories itself over a
|
|
102
|
+
forwarded agent. A warm deploy sends commands and secrets, nothing else.
|
|
103
|
+
- **Secrets stay out of the image.** The environment file and every credential
|
|
104
|
+
arrive as BuildKit `--secret` mounts, so they are in neither a layer nor
|
|
105
|
+
`docker history`.
|
|
106
|
+
- **An extensible pipeline.** Redkite's own four steps sit at ordinary points that
|
|
107
|
+
your config can hook around or replace outright.
|
|
108
|
+
- **Zero dependencies.** One package. Node reads the TypeScript config itself.
|
|
109
|
+
|
|
110
|
+
## Requirements
|
|
111
|
+
|
|
112
|
+
| Where | Needs |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| Your machine | Node 22.18 or newer, git, ssh, an ssh agent with a key that can reach your repositories |
|
|
115
|
+
| The deploy host | docker with BuildKit, git, ssh access |
|
|
116
|
+
|
|
117
|
+
Node 22.18 is the version that reads TypeScript without a loader, which is what
|
|
118
|
+
lets redkite ship with nothing in `dependencies`. If your config needs more than
|
|
119
|
+
Node resolves on its own, a `tsconfig` path or a `./thing.js` specifier pointing
|
|
120
|
+
at a `./thing.ts` file, redkite uses your project's own `tsx` when you have one.
|
|
121
|
+
|
|
122
|
+
Name the config `redkite.config.mts` if your `package.json` has no
|
|
123
|
+
`"type": "module"`: a `.ts` file in a CommonJS package is CommonJS, where
|
|
124
|
+
`import` is not legal. Redkite says so if you get it wrong. `.js` and `.mjs` work
|
|
125
|
+
too, and `--config` overrides the search, which otherwise walks up from where
|
|
126
|
+
you ran it to the root of the project.
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
`defineDeployment` validates at load, so a duplicate name, two apps on one
|
|
131
|
+
route, or a malformed hook point is a config that fails rather than a deploy
|
|
132
|
+
that stops half way through.
|
|
133
|
+
|
|
134
|
+
### Environments
|
|
135
|
+
|
|
136
|
+
One file each, beside the deployment, named `redkite.<environment>.config.ts`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
// redkite.production.config.ts
|
|
140
|
+
import { defineEnvironment } from "redkite-cd";
|
|
141
|
+
|
|
142
|
+
export default defineEnvironment({
|
|
143
|
+
branch: "main",
|
|
144
|
+
subnet: "10.20.0",
|
|
145
|
+
publicPort: 80,
|
|
146
|
+
host: { bastion: "deploy@acme.example" },
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Redkite finds them by name, so there is no list to keep in step, and
|
|
151
|
+
`redkite.config.ts` has no `environments` key at all: a second place to put a
|
|
152
|
+
set of them is a second place for them to disagree. A config that declares one
|
|
153
|
+
does not compile.
|
|
154
|
+
|
|
155
|
+
A deployment with only one environment can carry it instead, as `environment`:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
export default defineDeployment({
|
|
159
|
+
project: "acme",
|
|
160
|
+
environment: { branch: "main", subnet: "10.20.0", publicPort: 80 },
|
|
161
|
+
// ...
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
That is an override, not a default: it answers for whatever name the command
|
|
166
|
+
line asks for, and wins over any file that disagrees. Two environments means two
|
|
167
|
+
files.
|
|
168
|
+
|
|
169
|
+
Selected on the command line and threaded into every derived name.
|
|
170
|
+
|
|
171
|
+
| Field | Meaning |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `branch` | The git ref each app is built from |
|
|
174
|
+
| `subnet` | First three octets. Redkite allocates the fourth |
|
|
175
|
+
| `publicPort` | The only port anybody outside ever types |
|
|
176
|
+
| `host.bastion` | `user@address` of the deploy host. Absent means this machine |
|
|
177
|
+
| `extraHosts` | Hostname to address, added to every container beside the derived ones |
|
|
178
|
+
| `buildOn` | `"host"` by default. `"local"` compiles here and ships the image |
|
|
179
|
+
|
|
180
|
+
Images are built on the deploy host, which is where they are needed and costs
|
|
181
|
+
nothing to move them. A host too small to compile on can be told otherwise:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
production: { buildOn: "local", branch: "main", subnet: "10.0.0", publicPort: 80 }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The checkout, the Dockerfile and the build then happen on this machine, and the
|
|
188
|
+
finished image is streamed into `docker load` on the host down the connection
|
|
189
|
+
that is already open. Nothing is written to a disk at either end. The skip check
|
|
190
|
+
asks the host rather than this machine, so an image compiled here before but
|
|
191
|
+
never sent is still sent. It is ignored when the deploy host is this machine,
|
|
192
|
+
since there would be nothing to move.
|
|
193
|
+
|
|
194
|
+
`extraHosts` is for something the apps must resolve that redkite does not run:
|
|
195
|
+
a managed database, a legacy service, anything whose address is the thing that
|
|
196
|
+
differs between staging and production.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
extraHosts: { "db.internal": "10.55.0.250" },
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
A name the deployment already resolves, an app's container or a service alias,
|
|
203
|
+
is refused rather than silently overridden. Redirecting one of those would send
|
|
204
|
+
its traffic somewhere else and the deploy would still look like it worked.
|
|
205
|
+
|
|
206
|
+
### Where the files live
|
|
207
|
+
|
|
208
|
+
By default `redkite.config.ts` sits at the root of the project with the
|
|
209
|
+
environment files beside it, and a deploy run from anywhere inside walks up to
|
|
210
|
+
find them. `package.json` is where a repository says otherwise:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"redkite": { "directory": "deploy" }
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Then `deploy/redkite.config.ts` and `deploy/redkite.production.config.ts`. If
|
|
219
|
+
that directory holds no config, redkite stops and says so rather than carrying
|
|
220
|
+
on up the tree: naming a directory and not putting the files there is a mistake,
|
|
221
|
+
not a hint.
|
|
222
|
+
|
|
223
|
+
An environment that lives somewhere the naming convention would not find it can
|
|
224
|
+
be named outright, at whatever path it is at:
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"redkite": {
|
|
229
|
+
"environments": {
|
|
230
|
+
"production": "./infra/live.ts",
|
|
231
|
+
"staging": "./infra/staging.ts"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Paths are read against the `package.json` that names them. One that is not there
|
|
238
|
+
is refused rather than silently skipped, and an environment named here that also
|
|
239
|
+
sits beside the deployment is refused too: it comes from one place or the other.
|
|
240
|
+
|
|
241
|
+
### Apps
|
|
242
|
+
|
|
243
|
+
| Field | Meaning |
|
|
244
|
+
| --- | --- |
|
|
245
|
+
| `name` | Becomes the container name, cache keys, volumes and nginx upstream |
|
|
246
|
+
| `repo` | Clone URL, fetched over the forwarded agent rather than with a token |
|
|
247
|
+
| `route` | The nginx location. `/` is the catch-all; `/api/` has its prefix stripped |
|
|
248
|
+
| `port` | The port the app listens on inside its container |
|
|
249
|
+
| `build` | How the repository becomes an image. See presets below |
|
|
250
|
+
| `dir` | Where the app sits in the repository, when it is not the whole of it |
|
|
251
|
+
| `health` | Probed on the container itself, not through the proxy |
|
|
252
|
+
| `secrets` | One ref or several, merged in order, written to `.env` in the image |
|
|
253
|
+
| `files` | Container path to the item whose contents land there |
|
|
254
|
+
| `volumes` | Volume name to container path, for state that outlives a deploy |
|
|
255
|
+
|
|
256
|
+
### Building from a directory
|
|
257
|
+
|
|
258
|
+
An app names either a repository to clone or a directory already on this
|
|
259
|
+
machine. A CI job that has already checked the code out is one; so is the copy
|
|
260
|
+
you are editing.
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
{
|
|
264
|
+
name: "backend",
|
|
265
|
+
path: "./services/backend", // instead of repo
|
|
266
|
+
route: "/api/",
|
|
267
|
+
port: 3001,
|
|
268
|
+
// ...
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The path is read against the deployment file, not against wherever the command
|
|
273
|
+
was run, so a deploy from a workspace and one from the root build the same tree.
|
|
274
|
+
Nothing is cloned, checked out or cleaned: what is on disk is what ships, and
|
|
275
|
+
the `branch` an environment names is not consulted at all.
|
|
276
|
+
|
|
277
|
+
The release is the content of the working tree, taken with git's own addressing
|
|
278
|
+
over a scratch index. It covers what is committed, what is modified and what is
|
|
279
|
+
untracked, and honours `.gitignore` — which is the same set the build reads. So
|
|
280
|
+
an edit you never committed is a new release and gets built, and a change under
|
|
281
|
+
an ignored `node_modules` is not and does not:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
unchanged already built at 7797c4c, nothing rebuilt
|
|
285
|
+
uncommitted edit built from 9bba792, rebuilt
|
|
286
|
+
untracked file built from 587974b, rebuilt
|
|
287
|
+
ignored file already built at 9bba792, nothing rebuilt
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Somewhere outside git there is nothing to say any of that, so the deployment
|
|
291
|
+
says it instead:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
{
|
|
295
|
+
name: "backend",
|
|
296
|
+
path: "../checkout",
|
|
297
|
+
include: ["src", "package.json", "yarn.lock"],
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`include` names what ships, relative to `path`. It decides the release and the
|
|
302
|
+
build context together: BuildKit is handed a `.dockerignore` that holds
|
|
303
|
+
everything back and lets exactly these through, so a `node_modules` the release
|
|
304
|
+
says nothing about is not uploaded either. Asked to build a directory git knows
|
|
305
|
+
nothing about without one, redkite says so and shows the line to add rather than
|
|
306
|
+
guessing or refusing outright.
|
|
307
|
+
|
|
308
|
+
An `include` on a work tree is allowed too, and wins over `.gitignore`. That is
|
|
309
|
+
how a repository holding several things is narrowed to the one being built.
|
|
310
|
+
|
|
311
|
+
When the deploy host is another machine, an app built from a path forces the
|
|
312
|
+
build to happen here and the image to be shipped, the same as `--local`. The
|
|
313
|
+
source is on this machine, so the builder is too.
|
|
314
|
+
|
|
315
|
+
### Build presets
|
|
316
|
+
|
|
317
|
+
`nodeApp` and `nextApp` describe a two-stage build: a fat builder image with
|
|
318
|
+
caches mounted, and a slim runtime image the compiled output is copied into.
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
nodeApp({
|
|
322
|
+
builder: "22-alpine",
|
|
323
|
+
runtime: "24-alpine",
|
|
324
|
+
submodules: true,
|
|
325
|
+
steps: ["yarn db:generate", "yarn build"],
|
|
326
|
+
output: "/app/dist",
|
|
327
|
+
carry: ["/app/.generated"],
|
|
328
|
+
entrypoint: ["node", "/app/index.js"],
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The dependency install is not one of the `steps`. The preset copies
|
|
333
|
+
`package.json` and `yarn.lock` ahead of the source and installs against those
|
|
334
|
+
alone, so a commit that changes only source code reuses the layer. That is the
|
|
335
|
+
difference between a deploy and a cold build.
|
|
336
|
+
|
|
337
|
+
#### What a build is allowed not to produce
|
|
338
|
+
|
|
339
|
+
A `COPY` whose source is missing fails the build. That is right for the output,
|
|
340
|
+
which is the app itself, and wrong for a directory the repository may simply not
|
|
341
|
+
have. A `carry` entry says which it is:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
carry: [
|
|
345
|
+
"/app/.generated", // must exist, or the build failed
|
|
346
|
+
{ path: "/app/public", optional: true }, // skipped when it is not there
|
|
347
|
+
]
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`nextApp` marks `public` optional and `.next/static` required, because static is
|
|
351
|
+
what the server answers with for every chunk it built. A lockfile named in
|
|
352
|
+
`dependencies.files` is optional too: whether one is missing is the package
|
|
353
|
+
manager's to say, in its own words.
|
|
354
|
+
|
|
355
|
+
`nextApp` takes `standalone`, which says whether `next.config` sets
|
|
356
|
+
`output: "standalone"`. It defaults to `true`. A standalone build ships the
|
|
357
|
+
tree Next produced and runs it on node alone; without it the whole repository
|
|
358
|
+
ships and `next start` resolves its own dependencies, which is a much larger
|
|
359
|
+
image.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
nextApp({ standalone: false })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### An app in a directory of its own
|
|
366
|
+
|
|
367
|
+
A repository holding several apps names each one's directory:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
{ name: "web", dir: "apps/web", build: nextApp(), /* ... */ }
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
The whole repository is copied into the image, and `dir` is where the build
|
|
374
|
+
steps and the shipped command run. Every `/app` path the build spec names is
|
|
375
|
+
read against it, so a preset needs no change: `output: "/app/dist"` becomes
|
|
376
|
+
`/app/apps/web/dist`.
|
|
377
|
+
|
|
378
|
+
A Next standalone build traces from the workspace root, so the tree it emits
|
|
379
|
+
holds `apps/web/server.js` rather than `server.js`. `nextApp` declares that with
|
|
380
|
+
`keepsLayout`, and the runtime stage then starts the command at
|
|
381
|
+
`/app/apps/web` and lands `.next/static` and `public` beside it. A build whose
|
|
382
|
+
output flattens the app to the top of the tree, which is every `nodeApp`, leaves
|
|
383
|
+
`keepsLayout` unset and nothing moves.
|
|
384
|
+
|
|
385
|
+
**The dependency install stays at the repository root.** A workspace resolves
|
|
386
|
+
one lockfile for every package in it, so the install has to see all of them,
|
|
387
|
+
and `node_modules` is where that put it. An app with its own lockfile in a
|
|
388
|
+
subdirectory is not covered by `dir` alone.
|
|
389
|
+
|
|
390
|
+
### Services
|
|
391
|
+
|
|
392
|
+
Long-lived containers shared by the apps. The proxy is not one of them: apps
|
|
393
|
+
carry routes, routes imply exactly one proxy, so it is derived rather than
|
|
394
|
+
listed.
|
|
395
|
+
|
|
396
|
+
A service is adopted when it is the one the config describes, and recreated when
|
|
397
|
+
it is not. Each is created carrying a fingerprint of everything a recreate would
|
|
398
|
+
change, the rendered nginx configuration included, so changing the published
|
|
399
|
+
port or `maxBodySize` reaches the running container instead of sitting in a file
|
|
400
|
+
it was never created from. `redkite plan` reports the comparison without
|
|
401
|
+
changing anything:
|
|
402
|
+
|
|
403
|
+
```
|
|
404
|
+
services on the host
|
|
405
|
+
acme-staging-nginx created from an earlier version of this file
|
|
406
|
+
acme-staging-redis not there, will be created
|
|
407
|
+
|
|
408
|
+
a deploy converges these
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
services: [
|
|
413
|
+
redis({ address: 26, volumes: { data: "/data" } }),
|
|
414
|
+
postgres({ secrets: bitwarden("..."), environment: { POSTGRES_DB: "acme" } }),
|
|
415
|
+
];
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
`postgres` requires `secrets` because the image will not start without
|
|
419
|
+
`POSTGRES_PASSWORD`, and a service that cannot come up is not a useful default.
|
|
420
|
+
Whatever the ref resolves to is written to a file on the deploy host and handed
|
|
421
|
+
over with `--env-file`, so the password is never an argument in a command line
|
|
422
|
+
or a shell history. Settings that are not credentials, `POSTGRES_DB` and
|
|
423
|
+
`POSTGRES_USER`, go in `environment` instead.
|
|
424
|
+
|
|
425
|
+
### Secrets
|
|
426
|
+
|
|
427
|
+
An id is a pointer, not a credential, so it belongs in the config while the
|
|
428
|
+
credentials arrive at deploy time.
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
secrets: bitwarden("00000000-0000-4000-8000-000000000001"),
|
|
432
|
+
files: { "/app/service-account.json": bitwarden("...") },
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
A deployment reading from Bitwarden wants `BW_CLIENT_ID`, `BW_CLIENT_SECRET` and
|
|
436
|
+
`BW_PASSWORD` in the environment. One that does not needs nothing: redkite only
|
|
437
|
+
opens the stores your config actually names.
|
|
438
|
+
|
|
439
|
+
## Hooks
|
|
440
|
+
|
|
441
|
+
A run is one list of steps, each handed what the one before it answered with.
|
|
442
|
+
Redkite puts four in it. A step says where it runs, and nothing has to call it.
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
setup:before:<name>
|
|
446
|
+
setup redkite: creates the network, brings the services up
|
|
447
|
+
setup:<name>
|
|
448
|
+
setup:after:<name>
|
|
449
|
+
|
|
450
|
+
build:before:<name>
|
|
451
|
+
build redkite: resolves, checks out and builds every image
|
|
452
|
+
build:<name>
|
|
453
|
+
build:after:<name>
|
|
454
|
+
|
|
455
|
+
swap:before:<name>
|
|
456
|
+
swap redkite: moves the addresses, health checks, reverts
|
|
457
|
+
swap:<name>
|
|
458
|
+
swap:after:<name>
|
|
459
|
+
|
|
460
|
+
cleanup:before:<name>
|
|
461
|
+
cleanup redkite: removes the retired containers and old images
|
|
462
|
+
cleanup:<name>
|
|
463
|
+
cleanup:after:<name>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
**Nothing about redkite's four is privileged.** They sit at ordinary points, and a
|
|
467
|
+
config that registers a step at the same point replaces it. That is how one is
|
|
468
|
+
turned off: put something there that does less.
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { defineStep } from "redkite-cd";
|
|
472
|
+
|
|
473
|
+
export default defineDeployment({
|
|
474
|
+
// ...
|
|
475
|
+
steps: [
|
|
476
|
+
defineStep("build:after:sourcemaps", async (built, context) => {
|
|
477
|
+
for (const app of built.apps) {
|
|
478
|
+
context.task.detail(`${app.name} at ${app.release.slice(0, 7)}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return built;
|
|
482
|
+
}),
|
|
483
|
+
|
|
484
|
+
// A host somebody else prunes: redkite's cleanup never runs
|
|
485
|
+
defineStep("cleanup", (released) => ({
|
|
486
|
+
...released,
|
|
487
|
+
removed: [],
|
|
488
|
+
reclaimed: [],
|
|
489
|
+
})),
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
A step is handed the previous step's answer and the context: the config, the
|
|
495
|
+
topology, the host, the docker client, the secret stores, the log, and its own
|
|
496
|
+
progress row. **If a step throws, the run stops**, because everything after it
|
|
497
|
+
was written assuming the steps before did what they said.
|
|
498
|
+
|
|
499
|
+
The value grows as the run goes, so a late step reads everything above it:
|
|
500
|
+
`environment` from the start, `network` and `services` after setup, `apps` after
|
|
501
|
+
build, `ok` and `released` and `reverted` after swap, `removed` and
|
|
502
|
+
`reclaimed` after cleanup.
|
|
503
|
+
|
|
504
|
+
What a step is handed is decided by where it runs, so the example above compiles
|
|
505
|
+
with no annotation and `built.apps` is known to exist. A step at a point in a
|
|
506
|
+
phase nobody defined does not compile, and a replacement for one of redkite's four
|
|
507
|
+
has to answer with what the rest of the run expects.
|
|
508
|
+
|
|
509
|
+
### Migrations
|
|
510
|
+
|
|
511
|
+
A migration is a step like any other. `migrate()` answers with one at
|
|
512
|
+
`swap:before:migrate-<app>`, so it runs in the image that was just built, while
|
|
513
|
+
the old containers are still serving, and throws before anything retires.
|
|
514
|
+
|
|
515
|
+
```ts
|
|
516
|
+
import { migrate } from "redkite-cd";
|
|
517
|
+
|
|
518
|
+
export default defineDeployment({
|
|
519
|
+
steps: [migrate({ app: "backend", command: "yarn db:migrate" })],
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
The runtime image holds only what the app compiled to, so the command runs in
|
|
524
|
+
the builder stage instead. Every app keeps that stage as an image of its own,
|
|
525
|
+
which costs the export of layers the runtime build produced anyway and means
|
|
526
|
+
there is nothing to set before a step can use it.
|
|
527
|
+
|
|
528
|
+
A step naming an app the deployment does not have fails before the run starts
|
|
529
|
+
rather than half way through it.
|
|
530
|
+
|
|
531
|
+
#### The network a step runs on
|
|
532
|
+
|
|
533
|
+
A migration defaults to `host`: the deploy host's own network stack, which is
|
|
534
|
+
what reaches a database that machine already reaches. A database this deployment
|
|
535
|
+
runs as a service is somewhere else, so say so:
|
|
536
|
+
|
|
537
|
+
```ts
|
|
538
|
+
migrate({ app: "backend", command: "yarn db:migrate", network: "deployment" })
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
| `network` | Where the container is attached |
|
|
542
|
+
| --- | --- |
|
|
543
|
+
| `"host"` | The deploy host's own stack. The default |
|
|
544
|
+
| `"deployment"` | The network the apps and services run on, with every alias they resolve, so `postgres:5432` works |
|
|
545
|
+
| `"none"` | Nothing at all |
|
|
546
|
+
| `{ named: "..." }` | A network somebody else made |
|
|
547
|
+
|
|
548
|
+
`attachment()` answers with the same flags for a step of your own:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
import { attachment, defineStep } from "redkite-cd";
|
|
552
|
+
|
|
553
|
+
defineStep("swap:before:seed", async (built, context) => {
|
|
554
|
+
const image = built.apps.find((app) => app.name === "backend")?.builderTag;
|
|
555
|
+
if (!image) throw new Error("backend did not keep its builder");
|
|
556
|
+
|
|
557
|
+
await context.docker.runOrThrow(
|
|
558
|
+
["run --rm", ...attachment("deployment", context.topology), image, "yarn db:seed"].join(" "),
|
|
559
|
+
"seeding failed",
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
return built;
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Plugins
|
|
567
|
+
|
|
568
|
+
A plugin is a function answering with steps, the way `redis()` answers with a
|
|
569
|
+
service spec:
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
const slack = (webhook: string) => [
|
|
573
|
+
defineStep("swap:before:announce", async (built) => {
|
|
574
|
+
await fetch(webhook, { method: "POST", body: `deploying ${built.apps.length} apps` });
|
|
575
|
+
return built;
|
|
576
|
+
}),
|
|
577
|
+
defineStep("cleanup:after:notify", async (finished) => {
|
|
578
|
+
await fetch(webhook, { method: "POST", body: `released ${finished.released.join(", ")}` });
|
|
579
|
+
return finished;
|
|
580
|
+
}),
|
|
581
|
+
];
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
Read whatever it needs where the config is loaded, so a missing value is a
|
|
585
|
+
config that fails rather than a deploy that gets most of the way through:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
const webhook = process.env.SLACK_WEBHOOK;
|
|
589
|
+
if (!webhook) throw new Error("SLACK_WEBHOOK is not set");
|
|
590
|
+
|
|
591
|
+
export default defineDeployment({
|
|
592
|
+
// ...
|
|
593
|
+
steps: [...slack(webhook)],
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
## Verifying a build
|
|
598
|
+
|
|
599
|
+
`redkite verify` is the same host, the same services and the same images as a
|
|
600
|
+
deploy, stopping where one would start moving addresses. It brings the network
|
|
601
|
+
and the services up, builds every app, and runs what each app declares instead
|
|
602
|
+
of swapping.
|
|
603
|
+
|
|
604
|
+
An app declares its checks. Nothing else is needed:
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
{
|
|
608
|
+
name: "backend",
|
|
609
|
+
// ...
|
|
610
|
+
verify: {
|
|
611
|
+
steps: ["yarn db:migrate", "yarn test:integration"],
|
|
612
|
+
environment: { NODE_ENV: "test" },
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
The commands run in the **builder** image, which is the one holding the test
|
|
618
|
+
runner and the dev dependencies. The runtime image has neither, and installing
|
|
619
|
+
them at check time would test a different tree. They run on the deployment
|
|
620
|
+
network, so a check reaches postgres at `postgres`, by the same alias the app
|
|
621
|
+
itself uses. They run in order and one at a time: the first is usually what
|
|
622
|
+
brings the test database to the schema the rest expect.
|
|
623
|
+
|
|
624
|
+
A test environment is a file like any other, and it publishes nothing:
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
// redkite.test.config.ts
|
|
628
|
+
export default defineEnvironment({
|
|
629
|
+
branch: "pull-request",
|
|
630
|
+
subnet: "172.254.0",
|
|
631
|
+
});
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`publicPort` is absent because nothing in a verify run serves. That absence is
|
|
635
|
+
the whole declaration: an environment without one cannot deploy, so `plan` shows
|
|
636
|
+
it only the verify pipeline, leaves the proxy out of its service list, and
|
|
637
|
+
prints no nginx.
|
|
638
|
+
|
|
639
|
+
```
|
|
640
|
+
pipeline (verify)
|
|
641
|
+
setup redkite
|
|
642
|
+
build redkite
|
|
643
|
+
verify redkite backend
|
|
644
|
+
cleanup redkite
|
|
645
|
+
|
|
646
|
+
services on the host (verify)
|
|
647
|
+
acme-test-redis not there, will be created
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Two things are refused before anything is built: a `verify` where no app
|
|
651
|
+
declares checks, and a `deploy` to an environment that names no `publicPort`.
|
|
652
|
+
|
|
653
|
+
Cleanup still runs, which is what keeps a CI host's disk from filling: it
|
|
654
|
+
reclaims every image of these apps except the ones this run built.
|
|
655
|
+
|
|
656
|
+
`verify:before:` and `verify:after:` are ordinary hook points, and `--local`,
|
|
657
|
+
`--full` and `--verbose` work the same as for a deploy.
|
|
658
|
+
|
|
659
|
+
## CLI
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
redkite <command> [environment]
|
|
663
|
+
|
|
664
|
+
plan [environment] Print the derived topology, the pipeline and the nginx
|
|
665
|
+
deploy [environment] Build, swap, health check, and revert on failure
|
|
666
|
+
verify [environment] Bring the services up, build, and run each app's checks
|
|
667
|
+
|
|
668
|
+
--config <path> Defaults to redkite.config.ts at the root of the project
|
|
669
|
+
--local Build the images here and ship them to the host
|
|
670
|
+
--full No step view: every line of every step, in full
|
|
671
|
+
--verbose Every host command, and every line a build printed
|
|
672
|
+
--version Print the version and exit
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
The environment defaults to `staging`. `plan` reads the host to report drift and
|
|
676
|
+
changes nothing, so it is safe to run against production.
|
|
677
|
+
|
|
678
|
+
### Watching a deploy
|
|
679
|
+
|
|
680
|
+
On a terminal, `deploy` draws the run as a list of steps. The one running is
|
|
681
|
+
open and its output rolls under a title that stays put; a step that finishes
|
|
682
|
+
shuts to a single line carrying what it cost.
|
|
683
|
+
|
|
684
|
+
```
|
|
685
|
+
[01:18] ✔ setup 4s
|
|
686
|
+
[01:18] ✔ build 9s
|
|
687
|
+
[01:18] ❯ ▾ Building web yarn build 1m09s
|
|
688
|
+
│ #15 [builder 10/10] RUN yarn build
|
|
689
|
+
│ ▲ Next.js 15.1.6
|
|
690
|
+
│ Creating an optimized production build ...
|
|
691
|
+
|
|
692
|
+
↑↓ move · enter open · shift+↑ latest · + open · - collapse all · q quit
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
| Key | What it does |
|
|
696
|
+
| --- | --- |
|
|
697
|
+
| `↑` `↓` | Scroll the open step's output, then move between steps once it runs out |
|
|
698
|
+
| `enter` | Open or shut the step under the cursor. One opened by hand stays open when it finishes |
|
|
699
|
+
| `shift+↑` | Jump to the step running now, and follow it again |
|
|
700
|
+
| `shift+↓` | Jump to the first step |
|
|
701
|
+
| `+` | Open the step running now, from anywhere, and keep opening the ones after it |
|
|
702
|
+
| `-` | Shut everything, and stop opening what comes next |
|
|
703
|
+
| `q` | Stop the deploy. Press again to kill it |
|
|
704
|
+
|
|
705
|
+
The gutter counts the whole run; the number on the right counts the step, and
|
|
706
|
+
freezes at what it cost the moment it finishes. Nothing drawn on the alternate
|
|
707
|
+
screen survives it, so the run is written out again on the way out.
|
|
708
|
+
|
|
709
|
+
Colour separates where you are from what is happening. The step under the
|
|
710
|
+
cursor is cyan, the step running now is yellow, and a failed one is red and
|
|
711
|
+
outranks both, because that is the row you are looking for. The tick, the cross
|
|
712
|
+
and the arrow carry the step's own state, and the gutter, the timers and the
|
|
713
|
+
rule down the side of a log stay dim so the output reads above them. `NO_COLOR`
|
|
714
|
+
turns all of it off.
|
|
715
|
+
|
|
716
|
+
Every measurement happens before a colour is added: an escape is zero columns
|
|
717
|
+
wide, and a row measured with one in it is a row that wraps.
|
|
718
|
+
|
|
719
|
+
Every row is one terminal line. A line too long for the width is cut with an
|
|
720
|
+
ellipsis rather than wrapped, because a wrapped row pushes everything under it
|
|
721
|
+
out of a frame counted in rows. `--full` is the way to read the untrimmed thing:
|
|
722
|
+
no view, no collapsing, every line of every step streamed in full as it arrives.
|
|
723
|
+
|
|
724
|
+
A pipe, a CI log or `REDKITE_PLAIN=1` gets that same streamed form, with
|
|
725
|
+
`--verbose` adding every host command beside it.
|
|
726
|
+
|
|
727
|
+
Messages said outside a step show the most recent few. They used to all stay on
|
|
728
|
+
screen, which on a long run left no room for any step's output at all. All of
|
|
729
|
+
them are written out again when the view closes.
|
|
730
|
+
|
|
731
|
+
### Stopping a deploy
|
|
732
|
+
|
|
733
|
+
`q`, or ctrl+C without the view, asks the run to stop. Whatever command is in
|
|
734
|
+
flight is killed and the pipeline unwinds through its own failure path, so the
|
|
735
|
+
scratch directory on the host is removed and the connection is closed. The run
|
|
736
|
+
stops **between steps**, which is the unit that leaves the host in a state the
|
|
737
|
+
next deploy can read: a build that has not finished leaves the running
|
|
738
|
+
deployment exactly as it was.
|
|
739
|
+
|
|
740
|
+
**The deploy does not exit while the build is still running.** Every command runs
|
|
741
|
+
in a process group of its own, so one signal reaches the shell, the docker client
|
|
742
|
+
and the build behind it. Over ssh that group is on the other machine: killing the
|
|
743
|
+
local client there would only lose the reach, so each command records its group
|
|
744
|
+
as it starts and the signal is sent down the connection to it.
|
|
745
|
+
|
|
746
|
+
The first press sends `SIGTERM`. Every press after it sends `SIGKILL`. Each says
|
|
747
|
+
which, and the run keeps asking the host how many are still there until the
|
|
748
|
+
answer is none:
|
|
749
|
+
|
|
750
|
+
```
|
|
751
|
+
Stopping the build (SIGTERM). Press again to kill it
|
|
752
|
+
Waiting for 1 still running
|
|
753
|
+
Killing the build (SIGKILL). Nothing exits until it is gone
|
|
754
|
+
Stopped
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
A process that has taken `SIGKILL` and is still there is one redkite cannot end.
|
|
758
|
+
The fifth press says so and offers the way out rather than taking it, because
|
|
759
|
+
taking it leaves work running with nothing watching it:
|
|
760
|
+
|
|
761
|
+
```
|
|
762
|
+
This is not stopping. It has had SIGKILL and is still there.
|
|
763
|
+
Press again to leave redkite. That does not stop it: the build keeps running
|
|
764
|
+
on the host, may finish and tag an image no deploy is waiting for, and holds
|
|
765
|
+
the CPU and disk it is using. Nothing will clean up after it but you.
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
The sixth press leaves.
|
|
769
|
+
|
|
770
|
+
## How a deploy runs
|
|
771
|
+
|
|
772
|
+
Everything happens on the deploy host, over a single multiplexed ssh connection
|
|
773
|
+
with the agent forwarded.
|
|
774
|
+
|
|
775
|
+
1. **Setup.** Creates the network, then adopts or builds each service.
|
|
776
|
+
2. **Resolve.** `git remote update` into a mirror under `~/.cache/redkite`, then
|
|
777
|
+
`rev-parse` the branch. That commit is what the image is tagged by.
|
|
778
|
+
3. **Check out.** A working tree sharing the mirror's object store, submodules
|
|
779
|
+
followed to the branch `.gitmodules` names.
|
|
780
|
+
4. **Build.** The pipeline is rendered as a Dockerfile beside the checkout and
|
|
781
|
+
handed to the BuildKit already inside the daemon. Skipped entirely when the
|
|
782
|
+
host is already holding this exact image.
|
|
783
|
+
5. **`swap:before`.** Anything hung here runs while the old containers are still
|
|
784
|
+
serving. A migration is the usual one, and it throws, so a failure retires
|
|
785
|
+
nothing.
|
|
786
|
+
6. **Swap.** The running container moves to the retired address without being
|
|
787
|
+
stopped and is renamed, and only then does the live address belong to the new
|
|
788
|
+
one. Nginx keeps the retired container as a backup upstream.
|
|
789
|
+
7. **Check.** Each app is probed on itself. One failure reverts all of them.
|
|
790
|
+
8. **Cleanup.** Retired containers removed, superseded images reclaimed.
|
|
791
|
+
|
|
792
|
+
A `verify` run walks the same list without steps 5 to 7. In their place it runs
|
|
793
|
+
each app's declared checks, so nothing it does touches what is serving.
|
|
794
|
+
|
|
795
|
+
An image the host already holds is not rebuilt. The tag covers the commit, the
|
|
796
|
+
pipeline, the build spec, the environment file and every credential file, so a
|
|
797
|
+
change to any of them is a new image and a change to none of them is a swap with
|
|
798
|
+
no build at all.
|
|
799
|
+
|
|
800
|
+
## Design
|
|
801
|
+
|
|
802
|
+
Everything is derived from the app list, which is what makes the constants block
|
|
803
|
+
and the hand-written nginx template stop existing.
|
|
804
|
+
|
|
805
|
+
Redkite talks to a machine through one small port: run a shell command, write a
|
|
806
|
+
file. `sshHost` implements it over a multiplexed ssh connection, `localHost`
|
|
807
|
+
over child processes, and nothing else in the library knows which is in use.
|
|
808
|
+
That is why the whole orchestration is asserted against a recorder rather than a
|
|
809
|
+
server, and why deploying to another machine and deploying to this one differ in
|
|
810
|
+
nothing but which of the two the CLI constructs.
|
|
811
|
+
|
|
812
|
+
## Contributing
|
|
813
|
+
|
|
814
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the layout of the source, how the
|
|
815
|
+
tests are organised, and what to run before opening a pull request.
|
|
816
|
+
|
|
817
|
+
## License
|
|
818
|
+
|
|
819
|
+
[MIT](./LICENSE) © Joao Kdouk
|