@norskvideo/ctl-dev-kit 0.2.75 → 0.2.77

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.
@@ -292,6 +292,12 @@ export interface CanonicalBytes {
292
292
  integration: string;
293
293
  /** conventions/demo.sh — the scripts/demo shim a product with tests/demo.spec.ts carries. */
294
294
  demoShim: string;
295
+ /** conventions/server-install.sh and server-uninstall.sh — the server
296
+ * deployment kit's deployment/install.sh and uninstall.sh. Gated only for a
297
+ * copy that names its canonical source, so a product's own hand-written
298
+ * install.sh at that path is left to it. */
299
+ serverInstall?: string;
300
+ serverUninstall?: string;
295
301
  /** doc-guide/with-display.sh — the X-display wrapper BOTH docs workflows invoke
296
302
  * as `scripts/with-display.sh`. Gated because the workflow is: an ungated
297
303
  * hand copy is how build-docs.yml came to fail in both turnkeys, which never
@@ -312,6 +318,30 @@ export interface CanonicalBytes {
312
318
  bumpSdkApp?: string;
313
319
  }
314
320
 
321
+ export const SERVER_KIT_COPIES = [
322
+ { rel: "deployment/install.sh", source: "server-install.sh", key: "serverInstall" },
323
+ { rel: "deployment/uninstall.sh", source: "server-uninstall.sh", key: "serverUninstall" },
324
+ ] as const;
325
+
326
+ /** A copy names its source in its header; a product's own script does not. */
327
+ export const isServerKitCopy = (content: string, source: string): boolean =>
328
+ content.includes(`@norskvideo/ctl-dev-kit conventions/${source}`);
329
+
330
+ export function serverKitProblems(repoRoot: string, canonical: CanonicalBytes): string[] {
331
+ const problems: string[] = [];
332
+ for (const { rel, source, key } of SERVER_KIT_COPIES) {
333
+ const expected = canonical[key];
334
+ const path = join(repoRoot, rel);
335
+ if (expected === undefined || !existsSync(path)) continue;
336
+ const actual = readFileSync(path, "utf8");
337
+ if (!isServerKitCopy(actual, source) || actual === expected) continue;
338
+ problems.push(
339
+ `${rel} has drifted from @norskvideo/ctl-dev-kit conventions/${source} (${firstDiffLine(actual, expected)}). ${RESYNC}`,
340
+ );
341
+ }
342
+ return problems;
343
+ }
344
+
315
345
  // 05-demo s6: a product that has a demo (tests/demo.spec.ts) exposes it the
316
346
  // shared way — scripts/demo, the verbatim executable shim that re-execs under
317
347
  // the nix dev shell and delegates to `bun run demo`, and the package.json
@@ -893,6 +923,7 @@ export function checkDrift(repoRoot: string, canonical: CanonicalBytes, opts: Ch
893
923
  for (const p of docsSiteExposureProblems(existsSync(siteWorkflowPath), hasDocsSite(repoRoot), opts.repoName)) push(p);
894
924
 
895
925
  for (const p of demoProblems(repoRoot, canonical.demoShim)) push(p);
926
+ for (const p of serverKitProblems(repoRoot, canonical)) push(p);
896
927
  for (const p of productLogsProblems(repoRoot)) push(p);
897
928
  for (const p of bunVersionProblems(repoRoot, canonical.ctlShell)) push(p);
898
929
  for (const p of ctlPinProblems(repoRoot)) push(p);
@@ -922,6 +953,8 @@ if (import.meta.main) {
922
953
  smoke: readFileSync(join(import.meta.dir, "smoke.yml"), "utf8"),
923
954
  integration: readFileSync(join(import.meta.dir, "integration.yml"), "utf8"),
924
955
  demoShim: readFileSync(join(import.meta.dir, "demo.sh"), "utf8"),
956
+ serverInstall: readFileSync(join(import.meta.dir, "server-install.sh"), "utf8"),
957
+ serverUninstall: readFileSync(join(import.meta.dir, "server-uninstall.sh"), "utf8"),
925
958
  flakeSdkApp: readFileSync(join(import.meta.dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
926
959
  checksSdkApp: readFileSync(join(import.meta.dir, "checks.sdk-app.yml"), "utf8"),
927
960
  bumpSdkApp: readFileSync(join(import.meta.dir, "bump.sdk-app.yml"), "utf8"),
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env bash
2
+ # Install this product on a server, from a release directory written by
3
+ # deployment/package-release.sh. Takes a box with nothing of ours on it to a
4
+ # running instance; uninstall.sh (beside it) takes it back.
5
+ #
6
+ # Runs ON the server, as a sudo-capable user, from the release directory:
7
+ #
8
+ # export NORSK_ADMIN_PASSWORD=... # first install only: the web login (>=8 chars, >=1 digit)
9
+ # export <PARAMETER>=... # any launch parameter the template declares
10
+ # bash install.sh --license /path/to/licence.json
11
+ #
12
+ # Launch parameters are the product template's own: every parameter it declares
13
+ # is passed when a variable of that name is set, a required one must be, and
14
+ # the rest take the template's defaults. Secrets come from the environment,
15
+ # never the command line.
16
+ #
17
+ # Idempotent: each step checks before it acts, so a re-run after a failure picks
18
+ # up where it stopped, and a re-run with a newer release replaces the product (a
19
+ # stored template never sees a new image otherwise) and relaunches the instance
20
+ # in the same working directory, so its media and settings survive.
21
+ #
22
+ # Access (ACCESS, default tunnel): the web page stays off the public network and
23
+ # is reached over SSH, the shape a host that admits only SSH and media needs:
24
+ # ssh -L 8443:127.0.0.1:8443 <user>@<host> # then https://localhost:8443
25
+ # The proxy listens on PROXY_PORT (8443), the port the tunnel forwards, so the
26
+ # URLs norsk-ctl advertises are the ones the browser uses. ACCESS=public serves
27
+ # 443 to the world instead, at PUBLIC_HOST (default the detected public IP).
28
+ #
29
+ # Options (environment):
30
+ # INSTANCE_ID default: the product's name
31
+ # CERT_SOURCE, DOMAIN, CERT_EMAIL passed to the norsk-ctl installer
32
+ # RELAUNCH=1 replace a running instance, e.g. to apply
33
+ # a changed parameter
34
+ #
35
+ # Copied verbatim from @norskvideo/ctl-dev-kit conventions/server-install.sh and
36
+ # drift-gated: edit the dev-kit source and re-sync, never this copy.
37
+ set -euo pipefail
38
+
39
+ CTL=(sudo -u norsk env NORSK_CTL_CONFIG_DIR=/etc/norsk-ctl NORSK_CTL_STATE_DIR=/var/lib/norsk-ctl
40
+ NORSK_CTL_LOG_DIR=/var/log/norsk-ctl norsk-ctl)
41
+ CTL_WAIT_SECONDS="${CTL_WAIT_SECONDS:-90}"
42
+
43
+ info() { printf '\n==> %s\n' "$*"; }
44
+ oops() {
45
+ printf 'error: %s\n' "$*" >&2
46
+ exit 1
47
+ }
48
+ # As the service user with its configuration: the installer adds us to the
49
+ # norsk group, but not in this login session, and sudo drops the profile.d
50
+ # environment the CLI reads.
51
+ ctl() { "${CTL[@]}" "$@"; }
52
+
53
+ # Bounded by the clock, not by attempts: a CLI call against a daemon that is
54
+ # down can itself take many seconds.
55
+ wait_for_ctl() {
56
+ local deadline=$((SECONDS + CTL_WAIT_SECONDS))
57
+ while [ "${SECONDS}" -lt "${deadline}" ]; do
58
+ timeout 5 "${CTL[@]}" product list >/dev/null 2>&1 && return 0
59
+ sleep 1
60
+ done
61
+ oops "norsk-ctl did not come back within ${CTL_WAIT_SECONDS}s (journalctl -u norsk-ctl)"
62
+ }
63
+
64
+ # Appends `key: value` to a YAML file unless the key is set. On its own line
65
+ # whether or not the file ends in a newline: norsk-ctl's init writes
66
+ # config.yaml without one, and a key joined to the last line stops the daemon.
67
+ ensure_config_line() {
68
+ local key=$1 value=$2 file=$3
69
+ sudo grep -q "^${key}:" "${file}" && return 1
70
+ printf '\n%s: %s\n' "${key}" "${value}" | sudo tee -a "${file}" >/dev/null
71
+ }
72
+
73
+ # The template's parameters (`template show -o json` on stdin) as --param
74
+ # arguments, NUL-separated: a parameter is passed when a variable of its name is
75
+ # set and non-empty; INSTANCE_NAME defaults to the instance id. Fails naming
76
+ # every required parameter that is unset.
77
+ launch_params() {
78
+ local name required value missing=()
79
+ while IFS=$'\t' read -r name required; do
80
+ value="${!name:-}"
81
+ [ -z "${value}" ] && [ "${name}" = INSTANCE_NAME ] && value="${INSTANCE_ID}"
82
+ if [ -n "${value}" ]; then
83
+ printf -- '--param\0%s=%s\0' "${name}" "${value}"
84
+ elif [ "${required}" = true ]; then
85
+ missing+=("${name}")
86
+ fi
87
+ done < <(jq -r '.parameters[] | [.name, (.required // false | tostring)] | @tsv')
88
+ [ "${#missing[@]}" = 0 ] || oops "export the required launch parameters: ${missing[*]}"
89
+ }
90
+
91
+ main() {
92
+ local here
93
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
94
+ # shellcheck source=/dev/null
95
+ . "${here}/release.env" # PRODUCT, IMAGE, IMAGE_ARCH, IMAGE_TAR, CTL_VERSION, RELEASE_COMMIT
96
+
97
+ local license=""
98
+ while [ $# -gt 0 ]; do
99
+ case "$1" in
100
+ --license)
101
+ license=$2
102
+ shift
103
+ ;;
104
+ *)
105
+ echo "unknown argument $1" >&2
106
+ exit 2
107
+ ;;
108
+ esac
109
+ shift
110
+ done
111
+ INSTANCE_ID="${INSTANCE_ID:-${PRODUCT}}"
112
+ local access="${ACCESS:-tunnel}" proxy_port="${PROXY_PORT:-8443}"
113
+ case "${access}" in
114
+ tunnel | public) ;;
115
+ *)
116
+ echo "ACCESS must be tunnel or public, not ${access}" >&2
117
+ exit 2
118
+ ;;
119
+ esac
120
+ local record_dir="/var/lib/${PRODUCT}-install"
121
+ local record="${record_dir}/record.env"
122
+
123
+ info "Preflight"
124
+ [ -n "${license}" ] || oops "missing --license"
125
+ [ -r "${license}" ] || oops "cannot read ${license}"
126
+ # Docker is the box's own: the norsk-ctl installer would add it when missing,
127
+ # and uninstall.sh could then not tell whose it was.
128
+ for tool in curl jq docker tar gzip; do
129
+ command -v "${tool}" >/dev/null || oops "${tool} is required"
130
+ done
131
+ sudo -n true 2>/dev/null || sudo -v
132
+ local arch
133
+ case "$(uname -m)" in
134
+ x86_64) arch=amd64 ;;
135
+ aarch64 | arm64) arch=arm64 ;;
136
+ *) arch=unknown ;;
137
+ esac
138
+ [ "${arch}" = "${IMAGE_ARCH}" ] || oops "this release is ${IMAGE_ARCH}, this server is ${arch}"
139
+
140
+ # What the box looked like before us, so uninstall.sh restores exactly that.
141
+ # Written once: a re-run must not record our own changes as the baseline.
142
+ if ! sudo test -f "${record}"; then
143
+ info "Recording the baseline (${record})"
144
+ local invoking_user="${SUDO_USER:-$(id -un)}"
145
+ sudo install -d -m 0755 "${record_dir}"
146
+ {
147
+ printf 'RECORDED_AT=%q\n' "$(date -Is)"
148
+ printf 'INVOKING_USER=%q\n' "${invoking_user}"
149
+ printf 'USER_GROUPS_BEFORE=%q\n' "$(id -nG "${invoking_user}")"
150
+ printf 'CTL_PREINSTALLED=%q\n' "$(command -v norsk-ctl >/dev/null && echo 1 || echo 0)"
151
+ printf 'IMAGES_BEFORE=%q\n' "$(sudo docker image ls -q --no-trunc | sort -u | tr '\n' ' ')"
152
+ for key in net.core.rmem_max net.core.wmem_max net.core.rmem_default net.core.wmem_default \
153
+ net.core.netdev_max_backlog net.ipv4.tcp_rmem net.ipv4.tcp_wmem net.ipv4.tcp_congestion_control; do
154
+ printf 'SYSCTL_%s=%q\n' "$(echo "${key}" | tr '.' '_')" "$(sysctl -n "${key}" | tr '\t' ' ')"
155
+ done
156
+ } | sudo tee "${record}" >/dev/null
157
+ fi
158
+
159
+ if ! command -v norsk-ctl >/dev/null; then
160
+ info "norsk-ctl ${CTL_VERSION} (public installer)"
161
+ : "${NORSK_ADMIN_PASSWORD:?export NORSK_ADMIN_PASSWORD (>=8 chars, >=1 digit)}"
162
+ export NORSK_ADMIN_PASSWORD
163
+ local installer
164
+ installer="$(mktemp)"
165
+ curl -fsSL https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl/install.sh -o "${installer}"
166
+ local flags=()
167
+ [ -n "${CERT_SOURCE:-}" ] && flags+=(--cert-source "${CERT_SOURCE}")
168
+ [ -n "${DOMAIN:-}" ] && flags+=(--domain "${DOMAIN}")
169
+ [ -n "${CERT_EMAIL:-}" ] && flags+=(--cert-email "${CERT_EMAIL}")
170
+ if [ "${access}" = tunnel ]; then
171
+ flags+=(--public-host localhost --proxy-port "${proxy_port}" --no-http-redirect)
172
+ if grep -q -- '--bind-address)' "${installer}"; then
173
+ flags+=(--bind-address 127.0.0.1)
174
+ else
175
+ echo "warning: this norsk-ctl cannot bind to loopback; its proxy and API listen on every interface" >&2
176
+ fi
177
+ else
178
+ flags+=(--public-host "${PUBLIC_HOST:-auto}")
179
+ fi
180
+ bash "${installer}" --server --version "${CTL_VERSION}" --license "${license}" "${flags[@]}" --pull-images --yes
181
+ rm -f "${installer}"
182
+ fi
183
+ # The public installer leaves a running norsk-ctl as it is, so a box set up by
184
+ # an earlier release is upgraded here, as norsk-ctl's upgrade guide does it:
185
+ # back up its config and state, swap the binary, restart the service. The
186
+ # daemon's SIGTERM stops only itself; the proxy and instances keep running.
187
+ local installed_ctl
188
+ installed_ctl="$(norsk-ctl --version)"
189
+ if [ "${installed_ctl}" != "${CTL_VERSION}" ]; then
190
+ info "Upgrading norsk-ctl ${installed_ctl} -> ${CTL_VERSION}"
191
+ local plat base bin want backup
192
+ plat="linux-$([ "${arch}" = amd64 ] && echo x64 || echo arm64)"
193
+ base="https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl/${CTL_VERSION}"
194
+ bin="$(mktemp)"
195
+ curl -fsSL "${base}/norsk-ctl-${CTL_VERSION}-${plat}" -o "${bin}"
196
+ want="$(curl -fsSL "${base}/norsk-ctl-${CTL_VERSION}-${plat}.sha256" | awk '{print $1}')"
197
+ [ "$(sha256sum "${bin}" | awk '{print $1}')" = "${want}" ] || oops "norsk-ctl ${CTL_VERSION} failed its checksum"
198
+ backup="${record_dir}/norsk-ctl-${installed_ctl}.tar.gz"
199
+ sudo tar -czf "${backup}" -C / etc/norsk-ctl var/lib/norsk-ctl
200
+ echo "backed up norsk-ctl's config and state to ${backup}"
201
+ sudo install -o norsk -g norsk -m 0755 "${bin}" "/opt/norsk-ctl/bin/norsk-ctl-${CTL_VERSION}"
202
+ rm -f "${bin}"
203
+ sudo ln -sfn "/opt/norsk-ctl/bin/norsk-ctl-${CTL_VERSION}" /usr/local/bin/norsk-ctl
204
+ sudo systemctl restart norsk-ctl
205
+ wait_for_ctl
206
+ fi
207
+
208
+ # Behind a tunnel, norsk-ctl's shared MoQ relay would advertise its preview
209
+ # URLs from the tunnel's localhost, where no browser can reach them over UDP.
210
+ # An instance's own preview listener, given the box's public address, can.
211
+ local relay_dropin="/etc/systemd/system/norsk-ctl.service.d/${PRODUCT}.conf"
212
+ if [ "${access}" = tunnel ] && ! sudo test -f "${relay_dropin}"; then
213
+ info "Turning off norsk-ctl's shared MoQ relay"
214
+ sudo install -d /etc/systemd/system/norsk-ctl.service.d
215
+ printf '[Service]\nEnvironment=NORSK_CTL_MOQ_RELAY_ENABLE=0\n' | sudo tee "${relay_dropin}" >/dev/null
216
+ sudo systemctl daemon-reload
217
+ sudo systemctl restart norsk-ctl
218
+ wait_for_ctl
219
+ sudo docker rm -f norsk-moq-relay >/dev/null 2>&1 || true
220
+ fi
221
+
222
+ # Behind a tunnel the proxy and the daemon's API stay on loopback, on a
223
+ # norsk-ctl that can say so. An install made before it could gets the setting
224
+ # here; the restart republishes the proxy on it.
225
+ if [ "${access}" = tunnel ] && norsk-ctl init --help 2>/dev/null | grep -q -- '--bind-address' &&
226
+ ensure_config_line bindAddress 127.0.0.1 /etc/norsk-ctl/config.yaml; then
227
+ info "Bound the proxy and the daemon's API to loopback"
228
+ sudo systemctl restart norsk-ctl
229
+ wait_for_ctl
230
+ fi
231
+ local staged_license
232
+ staged_license="/etc/norsk-ctl/licenses/$(basename "${license}")"
233
+ sudo test -f "${staged_license}" || sudo install -m 0640 -o norsk -g norsk "${license}" "${staged_license}"
234
+
235
+ info "Product image ${IMAGE}"
236
+ sudo docker image inspect "${IMAGE}" >/dev/null 2>&1 || gunzip -c "${here}/${IMAGE_TAR}" | sudo docker load
237
+
238
+ info "Product ${PRODUCT}"
239
+ local registered
240
+ registered="$(ctl product list -o json |
241
+ jq -r --arg p "${PRODUCT}" '[.. | objects | select(has("spec") and .name == $p) | .spec.image][0] // ""')"
242
+ if [ -n "${registered}" ] && [ "${registered}" != "${IMAGE}" ]; then
243
+ # A stored template is fetched once at `product add`; a new image reaches
244
+ # nothing until the product is added again.
245
+ info "Replacing ${PRODUCT} (${registered} -> ${IMAGE})"
246
+ # Not purged: the relaunch below takes the same working directory.
247
+ if ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then ctl instance delete "${INSTANCE_ID}"; fi
248
+ for t in $(ctl template list -o json |
249
+ jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.productName == $p) | .name'); do
250
+ ctl template remove "${t}"
251
+ done
252
+ ctl product remove "${PRODUCT}"
253
+ registered=""
254
+ fi
255
+ [ -n "${registered}" ] || ctl product add --license-file "${staged_license}" --image "${IMAGE}"
256
+
257
+ info "Instance ${INSTANCE_ID}"
258
+ local template
259
+ template="$(ctl template list -o json |
260
+ jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.kind == "product-default" and .source.productName == $p) | .name' |
261
+ head -1)"
262
+ [ -n "${template}" ] || oops "no product-default template for ${PRODUCT}"
263
+ # Through files, not a pipe: launch_params then runs in this shell, and a
264
+ # missing required parameter stops the install before the instance is touched.
265
+ local shown params=()
266
+ shown="$(mktemp)"
267
+ ctl template show "${template}" -o json >"${shown}"
268
+ launch_params <"${shown}" >"${shown}.args"
269
+ mapfile -d '' params <"${shown}.args"
270
+ rm -f "${shown}" "${shown}.args"
271
+ if [ "${RELAUNCH:-}" = 1 ] && ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then
272
+ ctl instance delete "${INSTANCE_ID}"
273
+ fi
274
+ if ! ctl instance exists "${INSTANCE_ID}" >/dev/null 2>&1; then
275
+ # TODO: --param puts secrets on a process argv for the length of the call;
276
+ # move them to an env-file once ctl takes one.
277
+ ctl instance launch-template "${INSTANCE_ID}" --template "${template}" "${params[@]}"
278
+ fi
279
+
280
+ info "Done (release ${RELEASE_COMMIT})"
281
+ ctl instance list
282
+ echo
283
+ if [ "${access}" = tunnel ]; then
284
+ echo "Reach it with: ssh -L ${proxy_port}:127.0.0.1:${proxy_port} $(id -un)@<this host>, then https://localhost:${proxy_port}"
285
+ echo "Open inbound only the product's media ports (docs/administering.md lists them)."
286
+ else
287
+ echo "Open inbound: 443/tcp and the product's media ports (docs/administering.md lists them)."
288
+ fi
289
+ }
290
+
291
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env bash
2
+ # Take a server back to how it was before install.sh: the product, its
3
+ # instances and images, then norsk-ctl itself with everything its installer put
4
+ # on the box. Docker and anything else the box already had stay.
5
+ #
6
+ # bash uninstall.sh [--keep-ctl | --remove-ctl] [--yes]
7
+ #
8
+ # Runs from the release directory (release.env names the product). norsk-ctl is
9
+ # removed only when install.sh installed it (its baseline record says so) or no
10
+ # record exists, and never while another product is still registered.
11
+ # --keep-ctl removes only this product; --remove-ctl removes norsk-ctl even
12
+ # though it was there before install.sh.
13
+ #
14
+ # Ends by listing anything left behind, and exits non-zero if there is any.
15
+ #
16
+ # Copied verbatim from @norskvideo/ctl-dev-kit conventions/server-uninstall.sh
17
+ # and drift-gated: edit the dev-kit source and re-sync, never this copy.
18
+ set -euo pipefail
19
+
20
+ info() { printf '\n==> %s\n' "$*"; }
21
+ oops() {
22
+ printf 'error: %s\n' "$*" >&2
23
+ exit 1
24
+ }
25
+ ctl() {
26
+ sudo -u norsk env NORSK_CTL_CONFIG_DIR=/etc/norsk-ctl NORSK_CTL_STATE_DIR=/var/lib/norsk-ctl \
27
+ NORSK_CTL_LOG_DIR=/var/log/norsk-ctl norsk-ctl "$@"
28
+ }
29
+ have_ctl() { command -v norsk-ctl >/dev/null && systemctl is-active --quiet norsk-ctl; }
30
+
31
+ # Whether ctl goes too: asked to keep it, or it predates install.sh and nobody
32
+ # asked for it gone.
33
+ removes_ctl() {
34
+ local keep=$1 remove=$2 preinstalled=$3
35
+ [ "${keep}" = 1 ] && return 1
36
+ [ "${preinstalled}" = 1 ] && [ "${remove}" != 1 ] && return 1
37
+ return 0
38
+ }
39
+
40
+ main() {
41
+ local here
42
+ here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43
+ # shellcheck source=/dev/null
44
+ . "${here}/release.env" # PRODUCT, IMAGE
45
+ local keep_ctl=0 remove_ctl_flag=0 assume_yes=0
46
+ while [ $# -gt 0 ]; do
47
+ case "$1" in
48
+ --keep-ctl) keep_ctl=1 ;;
49
+ --remove-ctl) remove_ctl_flag=1 ;;
50
+ --yes | -y) assume_yes=1 ;;
51
+ *)
52
+ echo "unknown argument $1" >&2
53
+ exit 2
54
+ ;;
55
+ esac
56
+ shift
57
+ done
58
+ local record_dir="/var/lib/${PRODUCT}-install"
59
+ local record="${record_dir}/record.env"
60
+
61
+ sudo -n true 2>/dev/null || sudo -v
62
+ if sudo test -f "${record}"; then
63
+ # shellcheck source=/dev/null
64
+ . <(sudo cat "${record}")
65
+ else
66
+ echo "no baseline record (${record}): restoring stock defaults where the record would have said" >&2
67
+ fi
68
+ local remove_ctl=0
69
+ removes_ctl "${keep_ctl}" "${remove_ctl_flag}" "${CTL_PREINSTALLED:-0}" && remove_ctl=1
70
+
71
+ if [ "${assume_yes}" != 1 ]; then
72
+ printf 'Remove %s%s from this server? [y/N] ' "${PRODUCT}" "$([ "${remove_ctl}" = 1 ] && echo ' and norsk-ctl')"
73
+ local answer
74
+ read -r answer
75
+ [ "${answer}" = y ] || [ "${answer}" = Y ] || exit 1
76
+ fi
77
+
78
+ if have_ctl; then
79
+ info "Product ${PRODUCT}"
80
+ for id in $(ctl instance list -o json | jq -r --arg p "${PRODUCT}" '.instances[] | select(.productName == $p) | .id'); do
81
+ ctl instance delete "${id}" --purge
82
+ done
83
+ for t in $(ctl template list -o json |
84
+ jq -r --arg p "${PRODUCT}" '.productTemplates[] | select(.source.productName == $p) | .name'); do
85
+ ctl template remove "${t}"
86
+ done
87
+ if ctl product exists "${PRODUCT}" >/dev/null 2>&1; then ctl product remove "${PRODUCT}"; fi
88
+ fi
89
+ sudo docker rm -f "norsk-product-${PRODUCT}" >/dev/null 2>&1 || true
90
+
91
+ local proxy_images=""
92
+ if [ "${remove_ctl}" = 1 ]; then
93
+ if have_ctl; then
94
+ local others
95
+ others="$(ctl product list -o json | jq -r '[.. | objects | select(has("spec")) | .name] | join(" ")')"
96
+ [ -z "${others}" ] || oops "other products are registered (${others}); re-run with --keep-ctl"
97
+ fi
98
+
99
+ info "norsk-ctl"
100
+ # The proxy's images are whatever ctl pulled; read them before the
101
+ # containers go.
102
+ proxy_images="$(sudo docker ps -a --filter name=norsk-proxy --format '{{.Image}}' | sort -u)"
103
+ if have_ctl; then ctl shutdown || true; fi
104
+ sudo systemctl disable --now norsk-ctl.service 2>/dev/null || true
105
+ sudo rm -f /etc/systemd/system/norsk-ctl.service
106
+ sudo rm -rf /etc/systemd/system/norsk-ctl.service.d
107
+ sudo systemctl daemon-reload
108
+ sudo docker rm -f norsk-proxy norsk-proxy-oauth2 norsk-moq-relay >/dev/null 2>&1 || true
109
+ sudo docker network rm norsk-net >/dev/null 2>&1 || true
110
+
111
+ local working_dir
112
+ working_dir="$(sudo awk '/^defaultWorkingDirectory:/ { print $2 }' /etc/norsk-ctl/config.yaml 2>/dev/null || true)"
113
+ sudo rm -rf /etc/norsk-ctl /var/lib/norsk-ctl /var/log/norsk-ctl /opt/norsk-ctl \
114
+ /usr/local/bin/norsk-ctl /etc/profile.d/norsk-ctl.sh "${working_dir:-/var/norsk-ctl}"
115
+ # TODO: certbot (--cert-source certbot): delete the certificate and the
116
+ # renewal timer the installer set up.
117
+
118
+ info "Kernel tuning"
119
+ sudo rm -f /etc/sysctl.d/90-norsk-tuning.conf
120
+ # Stock Ubuntu 24.04 values stand in when there is no record. Applied before
121
+ # `sysctl --system`, so the box's own sysctl.d settings still win.
122
+ restore() { sudo sysctl -q -w "$1=$2" >/dev/null || echo "warning: could not restore $1" >&2; }
123
+ restore net.core.rmem_max "${SYSCTL_net_core_rmem_max:-212992}"
124
+ restore net.core.wmem_max "${SYSCTL_net_core_wmem_max:-212992}"
125
+ restore net.core.rmem_default "${SYSCTL_net_core_rmem_default:-212992}"
126
+ restore net.core.wmem_default "${SYSCTL_net_core_wmem_default:-212992}"
127
+ restore net.core.netdev_max_backlog "${SYSCTL_net_core_netdev_max_backlog:-1000}"
128
+ restore net.ipv4.tcp_rmem "${SYSCTL_net_ipv4_tcp_rmem:-4096 131072 6291456}"
129
+ restore net.ipv4.tcp_wmem "${SYSCTL_net_ipv4_tcp_wmem:-4096 16384 4194304}"
130
+ restore net.ipv4.tcp_congestion_control "${SYSCTL_net_ipv4_tcp_congestion_control:-cubic}"
131
+ sudo sysctl -q --system >/dev/null
132
+
133
+ info "Service user"
134
+ # Deleting the norsk group also takes every member out of it. Docker
135
+ # membership the installer granted goes only when the record shows it was
136
+ # not there before.
137
+ if [ -n "${INVOKING_USER:-}" ] && ! grep -qw docker <<<"${USER_GROUPS_BEFORE:-docker}"; then
138
+ sudo gpasswd -d "${INVOKING_USER}" docker >/dev/null || true
139
+ fi
140
+ if id -u norsk >/dev/null 2>&1; then sudo userdel -r norsk 2>/dev/null || true; fi
141
+ if getent group norsk >/dev/null; then sudo groupdel norsk || true; fi
142
+ fi
143
+
144
+ info "Images"
145
+ # Ours, the media and Studio images its instances ran, and the proxy's;
146
+ # anything the record shows was here before stays.
147
+ local product_repo="${IMAGE%:*}" id
148
+ for ref in $(sudo docker image ls --format '{{.Repository}}:{{.Tag}}' |
149
+ grep -E "^(${product_repo}|norskvideo/norsk|norskvideo/norsk-studio|norskvideo/norsk-moq-relay):" || true) ${proxy_images}; do
150
+ id="$(sudo docker image inspect -f '{{.Id}}' "${ref}" 2>/dev/null)" || continue
151
+ grep -qw "${id}" <<<"${IMAGES_BEFORE:-}" && continue
152
+ sudo docker image rm "${ref}" >/dev/null || echo "warning: could not remove ${ref}" >&2
153
+ done
154
+
155
+ sudo rm -rf "${record_dir}"
156
+
157
+ info "Left behind"
158
+ local left=0
159
+ check() {
160
+ if eval "$2"; then
161
+ echo " $1"
162
+ left=1
163
+ fi
164
+ }
165
+ check "image ${product_repo}" "sudo docker image ls -q '${product_repo}' | grep -q ."
166
+ check "container norsk-product-${PRODUCT}" "sudo docker ps -aq --filter name=norsk-product-${PRODUCT} | grep -q ."
167
+ if [ "${remove_ctl}" = 1 ]; then
168
+ for p in /etc/norsk-ctl /var/lib/norsk-ctl /var/log/norsk-ctl /opt/norsk-ctl /usr/local/bin/norsk-ctl \
169
+ /etc/profile.d/norsk-ctl.sh /etc/sysctl.d/90-norsk-tuning.conf /etc/systemd/system/norsk-ctl.service \
170
+ /etc/systemd/system/norsk-ctl.service.d; do
171
+ check "${p}" "sudo test -e '${p}'"
172
+ done
173
+ check "user norsk" "id -u norsk >/dev/null 2>&1"
174
+ check "group norsk" "getent group norsk >/dev/null"
175
+ check "docker network norsk-net" "sudo docker network inspect norsk-net >/dev/null 2>&1"
176
+ check "norsk containers" "sudo docker ps -a --format '{{.Names}}' | grep -q '^norsk-'"
177
+ fi
178
+ [ "${left}" = 0 ] && echo " nothing"
179
+ exit "${left}"
180
+ }
181
+
182
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then main "$@"; fi
@@ -44,6 +44,7 @@ import {
44
44
  GITIGNORE_END,
45
45
  hasDocsSite,
46
46
  importsCtlShell,
47
+ isServerKitCopy,
47
48
  PRODUCT_LINE,
48
49
  PRODUCT_SENTINEL,
49
50
  type ProductShape,
@@ -53,6 +54,7 @@ import {
53
54
  RUNNER_LINE,
54
55
  resolveRepoName,
55
56
  resolveShape,
57
+ SERVER_KIT_COPIES,
56
58
  splitAtMarkers,
57
59
  } from "./check-drift.ts";
58
60
 
@@ -334,6 +336,18 @@ function syncDemoShim(repoRoot: string, canonical: string, r: SyncReport): void
334
336
  chmodSync(path, 0o755);
335
337
  }
336
338
 
339
+ // The server kit's scripts: only a copy (it names its source) is rewritten; a
340
+ // product's own install.sh at that path is the product's.
341
+ export function syncServerKit(repoRoot: string, canonical: CanonicalBytes, r: SyncReport): void {
342
+ for (const { rel, source, key } of SERVER_KIT_COPIES) {
343
+ const expected = canonical[key];
344
+ const path = join(repoRoot, rel);
345
+ if (expected === undefined || !existsSync(path) || !isServerKitCopy(readFileSync(path, "utf8"), source)) continue;
346
+ writeIfChanged(path, expected, rel, r.written);
347
+ chmodSync(path, 0o755);
348
+ }
349
+ }
350
+
337
351
  // Both docs workflows run `bash scripts/with-display.sh ...`. It stays a
338
352
  // repo-local executable (the package.json guide scripts invoke it by that path),
339
353
  // so a repo that carries a docs workflow gets the canonical copy written here
@@ -478,6 +492,7 @@ export function syncDrift(
478
492
  syncGitignore(repoRoot, canonical.gitignoreCore, r);
479
493
  syncDprint(repoRoot, canonical.dprint, r);
480
494
  syncDemoShim(repoRoot, canonical.demoShim, r);
495
+ syncServerKit(repoRoot, canonical, r);
481
496
  syncWithDisplay(repoRoot, canonical.withDisplay, r);
482
497
  syncDocsTheme(repoRoot, r);
483
498
  syncPublishDocsSite(repoRoot, canonical.publishDocsSite, opts.repoName, r);
@@ -508,6 +523,8 @@ if (import.meta.main) {
508
523
  smoke: readFileSync(join(dir, "smoke.yml"), "utf8"),
509
524
  integration: readFileSync(join(dir, "integration.yml"), "utf8"),
510
525
  demoShim: readFileSync(join(dir, "demo.sh"), "utf8"),
526
+ serverInstall: readFileSync(join(dir, "server-install.sh"), "utf8"),
527
+ serverUninstall: readFileSync(join(dir, "server-uninstall.sh"), "utf8"),
511
528
  flakeSdkApp: readFileSync(join(dir, "..", "build", "flake.sdk-app.nix"), "utf8"),
512
529
  checksSdkApp: readFileSync(join(dir, "checks.sdk-app.yml"), "utf8"),
513
530
  bumpSdkApp: readFileSync(join(dir, "bump.sdk-app.yml"), "utf8"),
@@ -33,6 +33,10 @@ export interface Canon {
33
33
  invariantsTemplate: string;
34
34
  /** conventions/demo.sh, emitted as scripts/demo. */
35
35
  demoShim: string;
36
+ /** conventions/server-install.sh and server-uninstall.sh, emitted as
37
+ * deployment/install.sh and deployment/uninstall.sh. */
38
+ serverInstall: string;
39
+ serverUninstall: string;
36
40
  /** doc-guide/with-display.sh, emitted as scripts/with-display.sh — the docs
37
41
  * workflow invokes it by that repo-local path. */
38
42
  withDisplay: string;
@@ -58,6 +62,8 @@ export function loadCanon(): Canon {
58
62
  gitignoreCore: readFileSync(join(conventionsDir, "gitignore.core"), "utf8"),
59
63
  invariantsTemplate: readFileSync(join(import.meta.dir, "..", "testing", "INVARIANTS.template.md"), "utf8"),
60
64
  demoShim: readFileSync(join(conventionsDir, "demo.sh"), "utf8"),
65
+ serverInstall: readFileSync(join(conventionsDir, "server-install.sh"), "utf8"),
66
+ serverUninstall: readFileSync(join(conventionsDir, "server-uninstall.sh"), "utf8"),
61
67
  };
62
68
  }
63
69
 
@@ -0,0 +1,95 @@
1
+ // The server deployment kit: how a turnkey reaches a customer's own server and
2
+ // leaves it again. install.sh and uninstall.sh are product-independent (they
3
+ // read the product from release.env and its launch parameters from the stored
4
+ // template), so they are canonical copies of conventions/server-*.sh. What is
5
+ // this product's is the image the release packs.
6
+ import type { ShapeContext } from "./create-product.ts";
7
+
8
+ export function packageReleaseSh(ctx: ShapeContext): string {
9
+ return `#!/usr/bin/env bash
10
+ # Build a release for a server: the product image as a portable tar, install.sh
11
+ # and uninstall.sh, and release.env naming what they install. The image is in
12
+ # no registry; the server pulls the media and Studio images and norsk-ctl
13
+ # itself.
14
+ #
15
+ # bun run package-release [-- <out-dir>] # default release/
16
+ # SKIP_BUILD=1 IMAGE_TAG=<repo:tag> bun run package-release # an existing image
17
+ #
18
+ # Then copy the directory to the server and run install.sh there (its header
19
+ # says how), e.g.
20
+ # tar -C release -czf - . | ssh <host> 'mkdir -p ${ctx.name}-release && tar -C ${ctx.name}-release -xzf -'
21
+ set -euo pipefail
22
+
23
+ repo="$(cd "$(dirname "\${BASH_SOURCE[0]}")/.." && pwd)"
24
+ out="\${1:-\${repo}/release}"
25
+ cd "\${repo}"
26
+ commit="$(git describe --always --dirty)"
27
+ # Tagged by commit: install.sh tells releases apart by tag, and a reused tag
28
+ # (\`:dev\`) would leave a server on the build it already has. The licence names
29
+ # the repository, not the tag.
30
+ export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:\${commit}}"
31
+ [ -n "\${SKIP_BUILD:-}" ] || bun run build:image
32
+ # The tested norsk-ctl is the one the dev shell pins.
33
+ ctl_version="$(sed -n 's/^ *ctlVersion = "\\(.*\\)";/\\1/p' flake.nix)"
34
+ [ -n "\${ctl_version}" ] || {
35
+ echo "no ctlVersion in flake.nix" >&2
36
+ exit 1
37
+ }
38
+
39
+ rm -rf "\${out}"
40
+ mkdir -p "\${out}"
41
+ docker save "\${IMAGE_TAG}" | gzip -1 >"\${out}/image.tar.gz"
42
+ cp deployment/install.sh deployment/uninstall.sh "\${out}/"
43
+ {
44
+ printf 'PRODUCT=%q\\n' ${ctx.name}
45
+ printf 'IMAGE=%q\\n' "\${IMAGE_TAG}"
46
+ printf 'IMAGE_ARCH=%q\\n' "$(docker image inspect -f '{{.Architecture}}' "\${IMAGE_TAG}")"
47
+ printf 'IMAGE_TAR=%q\\n' image.tar.gz
48
+ printf 'CTL_VERSION=%q\\n' "\${ctl_version}"
49
+ printf 'RELEASE_COMMIT=%q\\n' "\${commit}"
50
+ } >"\${out}/release.env"
51
+ echo "release in \${out}"
52
+ `;
53
+ }
54
+
55
+ export function shipImageSh(ctx: ShapeContext): string {
56
+ return `#!/usr/bin/env bash
57
+ # Build the ${ctx.name} image and load it into a remote host's Docker, for a
58
+ # host that cannot pull it (the image is in no registry). norsk-ctl on that host
59
+ # then adds and launches it from the local copy: \`product add\` and the launch
60
+ # both treat a failed pull as "use the local image". For a customer's server,
61
+ # package-release.sh is the whole kit; this is the quick route for a dev box.
62
+ #
63
+ # bun run ship-image -- <ssh-host> # build, then ship
64
+ # SKIP_BUILD=1 bun run ship-image -- <ssh-host> # ship the existing image
65
+ set -euo pipefail
66
+
67
+ host="\${1:?usage: ship-image.sh <ssh-host>}"
68
+ export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:dev}"
69
+ cd "$(dirname "\${BASH_SOURCE[0]}")/.."
70
+
71
+ [ -n "\${SKIP_BUILD:-}" ] || bun run build:image
72
+
73
+ # An image built for another architecture loads without complaint and then
74
+ # fails at launch, so refuse it here.
75
+ local_arch="$(docker image inspect -f '{{.Architecture}}' "\${IMAGE_TAG}")"
76
+ case "$(ssh "\${host}" uname -m)" in
77
+ x86_64) remote_arch=amd64 ;;
78
+ aarch64 | arm64) remote_arch=arm64 ;;
79
+ *) remote_arch=unknown ;;
80
+ esac
81
+ if [ "\${local_arch}" != "\${remote_arch}" ]; then
82
+ echo "\${IMAGE_TAG} is \${local_arch} but \${host} is \${remote_arch}: build on a \${remote_arch} machine" >&2
83
+ exit 1
84
+ fi
85
+
86
+ echo "shipping \${IMAGE_TAG} to \${host}"
87
+ docker save "\${IMAGE_TAG}" | gzip | ssh "\${host}" 'gunzip | docker load'
88
+ echo "loaded. On \${host}: norsk-ctl product add --image \${IMAGE_TAG} --license-file <licence>"
89
+ `;
90
+ }
91
+
92
+ export const SERVER_KIT_GITIGNORE = `# A release (bun run package-release): the image tar and the scripts, built for
93
+ # a server, never source.
94
+ release/
95
+ `;
@@ -38,6 +38,7 @@ import {
38
38
  } from "./features/dashboard.ts";
39
39
  import { CONFIG_SCREEN_URL, FRONTEND_SERVER_BLOCK, frontendFiles } from "./features/frontend.ts";
40
40
  import { VIEWS_PROXY_EXPOSE, VIEWS_RUNTIME_SCREEN, viewsFiles } from "./features/views.ts";
41
+ import { packageReleaseSh, SERVER_KIT_GITIGNORE, shipImageSh } from "./server-kit.ts";
41
42
 
42
43
  // The per-feature section of the generated CLAUDE.md: what each optional
43
44
  // workspace is, and the trap that comes with it. Every line here is something
@@ -169,11 +170,16 @@ pinned in \`flake.nix\` and put on PATH by the nix shell:
169
170
  \`norsk-ctl product add --dev-url http://localhost:${ctx.devPort}\` registers the
170
171
  running backend as a dev product.
171
172
 
173
+ **Licence:** commit a non-production dev licence for this product at
174
+ licensing/${ctx.name}-dev.json and \`bun run iterate\` uses it whenever
175
+ \`NORSK_LICENSE_FILE\` is unset; export it yourself for \`bun run demo\`.
176
+
172
177
  **Demo:** \`bun run demo -- up\` runs \`tests/demo.spec.ts\` (the
173
178
  @norskvideo/ctl-test-harness demo driver) from source on a private daemon —
174
179
  dev backend, registration, launch, the camera1 sample on the ingest, ready
175
180
  gates, URLs; Ctrl-C tears down. \`check\` is the headless form CI runs,
176
- \`spec\` prints the plan. \`scripts/demo\` is the same entry re-exec'd under
181
+ \`spec\` prints the plan. \`DEMO_SOURCES=none\` skips the sample, for feeds
182
+ pushed in by hand. \`scripts/demo\` is the same entry re-exec'd under
177
183
  \`nix develop .#dev\` when the pinned ctl is not on PATH.
178
184
 
179
185
  **Outer loop (real image):** \`bun run iterate\` — \`bun run build:image\` (the
@@ -182,6 +188,15 @@ container port 4321), then \`bun run demo -- up --mode image --daemon reuse\`:
182
188
  the driver deletes the instance, removes the template + product, re-adds the
183
189
  fresh image and checks the pins before relaunching.
184
190
 
191
+ **A customer's server:** \`bun run package-release\` writes \`release/\`: the
192
+ image as a tar (tagged by commit, so a server tells releases apart), the
193
+ drift-gated \`deployment/install.sh\` and \`deployment/uninstall.sh\`, and
194
+ \`release.env\`. Copied to the server, \`install.sh --license <file>\` takes a
195
+ bare box to a running instance and upgrades one in place (norsk-ctl too,
196
+ media kept); every template parameter is taken from a same-named environment
197
+ variable. \`uninstall.sh\` restores the box and lists anything left. A dev box
198
+ with no registry access: \`bun run ship-image -- <ssh-host>\`.
199
+
185
200
  ## First run after generation
186
201
 
187
202
  1. \`nix develop\` — the dev shell: pinned bun, biome and norsk-ctl.
@@ -359,6 +374,8 @@ function rootPackageJson(ctx: ShapeContext): string {
359
374
  ...(hasFrontend ? { "dev:frontend": "bun run --cwd frontend dev" } : {}),
360
375
  start: "bun run --cwd backend start",
361
376
  iterate: "bash deployment/iterate.sh",
377
+ "package-release": "bash deployment/package-release.sh",
378
+ "ship-image": "bash deployment/ship-image.sh",
362
379
  demo: "ctl-demo",
363
380
  },
364
381
  devDependencies: {
@@ -1805,7 +1822,8 @@ function iterateSh(ctx: ShapeContext): string {
1805
1822
  # instance; your daemon and the registration stay.
1806
1823
  #
1807
1824
  # Needs the daemon already running (\`norsk-ctl serve\`), \`norsk-ctl\` on PATH
1808
- # (\`nix develop .#dev\` provides it) and NORSK_LICENSE_FILE.
1825
+ # (\`nix develop .#dev\` provides it) and a licence: NORSK_LICENSE_FILE, else
1826
+ # the committed non-production dev licence at licensing/${ctx.name}-dev.json.
1809
1827
  #
1810
1828
  # Usage (from the repo root):
1811
1829
  # bun run iterate # full loop
@@ -1820,6 +1838,10 @@ repo="$(cd "$here/.." && pwd)"
1820
1838
  # directly (build-image.sh honours IMAGE_TAG).
1821
1839
  export IMAGE_TAG="\${IMAGE_TAG:-${ctx.name}:dev}"
1822
1840
 
1841
+ if [ -z "\${NORSK_LICENSE_FILE:-}" ] && [ -f "$repo/licensing/${ctx.name}-dev.json" ]; then
1842
+ export NORSK_LICENSE_FILE="$repo/licensing/${ctx.name}-dev.json"
1843
+ fi
1844
+
1823
1845
  if [ -n "\${SKIP_BUILD:-}" ]; then
1824
1846
  echo "=== build (skipped, SKIP_BUILD set) ==="
1825
1847
  else
@@ -1896,7 +1918,12 @@ export default defineDemo({
1896
1918
  image: process.env.IMAGE_TAG ?? "${ctx.name}:dev",
1897
1919
  template: { name: "default" },
1898
1920
  launch: { hardware: "none" },
1899
- sources: [{ name: "input", asset: { preset: "camera1" }, ingest: { param: "INGEST_PORT" }, streamId: "input" }],
1921
+ // DEMO_SOURCES=none leaves the ingest to feeds someone pushes in by hand
1922
+ // (stream id input), so they can be stopped and started live.
1923
+ sources:
1924
+ process.env.DEMO_SOURCES === "none"
1925
+ ? []
1926
+ : [{ name: "input", asset: { preset: "camera1" }, ingest: { param: "INGEST_PORT" }, streamId: "input" }],
1900
1927
  ready: [{ http: { studio: "/live/api/components" }, status: 200, timeoutMs: 120_000 }],
1901
1928
  open: [
1902
1929
  { name: "Studio", url: { proxy: "/instance/{id}/studio/" } },
@@ -1936,6 +1963,7 @@ shared/dist
1936
1963
  .git
1937
1964
  .github
1938
1965
  *.md
1966
+ release
1939
1967
  `;
1940
1968
 
1941
1969
  function exampleInputJson(_ctx: ShapeContext): string {
@@ -2240,6 +2268,7 @@ function gitignoreTail(ctx: ShapeContext): string {
2240
2268
  if (ctx.features.has("components")) blocks.push(COMPONENTS_GITIGNORE);
2241
2269
  if (ctx.features.has("dashboard")) blocks.push(DASHBOARD_GITIGNORE);
2242
2270
  blocks.push(DOCS_SITE_GITIGNORE);
2271
+ blocks.push(SERVER_KIT_GITIGNORE);
2243
2272
  return blocks.join("\n");
2244
2273
  }
2245
2274
 
@@ -2259,6 +2288,10 @@ export const turnkey: ShapeModule = {
2259
2288
  { path: ".dockerignore", content: DOCKERIGNORE },
2260
2289
  { path: "examples/default/input.json", content: exampleInputJson(ctx) },
2261
2290
  { path: "deployment/iterate.sh", content: iterateSh(ctx), executable: true },
2291
+ { path: "deployment/package-release.sh", content: packageReleaseSh(ctx), executable: true },
2292
+ { path: "deployment/ship-image.sh", content: shipImageSh(ctx), executable: true },
2293
+ { path: "deployment/install.sh", content: ctx.canon.serverInstall, executable: true },
2294
+ { path: "deployment/uninstall.sh", content: ctx.canon.serverUninstall, executable: true },
2262
2295
  { path: "scripts/demo", content: ctx.canon.demoShim, executable: true },
2263
2296
  { path: "README.md", content: readmeMd(ctx) },
2264
2297
  ...docsSiteFiles(ctx),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.75",
3
+ "version": "0.2.77",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",