feat(deploy): halkyon.li's first start, its .env made on the host (ADR 0039, D21)
Some checks failed
CI / check (push) Has been cancelled

first-start.md takes the instance from a bare host to running, once: the
sidecars' users, deploy with a read-only key to the forge and the
developer's keys (GPG for redeploy.sh's check, SSH pinned to it), .env,
SimpleLogin's app, the sidecars' folders and Jellyfin moved in, a role and
database for each app's server, the first start, the first operator by
ADR 0021's SQL, and the backups.

halkyon-li-env.sh writes .env with every secret made on the host and never
over an existing file, as the peppers must not change once people sign in:
SimpleLogin alone with approval, everything running behind the forward,
Train's map and routes over southern Bavaria and its search over Germany
until the Instance console cuts regions, Germany as where data and backups
are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-08 22:00:51 +01:00
commit 916799c051
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -0,0 +1,107 @@
import 'dart:io';
import 'package:test/test.dart';
/// halkyon.li's first start (deploy/host/first-start.md, ADR 0039) against what the
/// compose files run: the sidecars' users as their containers run, their folders
/// where the containers mount them, and a role and database for each app's server.
void main() {
final steps = File('deploy/host/first-start.md').readAsStringSync();
String compose(String name) => File('deploy/$name').readAsStringSync();
/// The users step 1 makes, by name: their IDs.
final users = {
for (final match in RegExp(
r'(\w+):(\d{5})',
).allMatches(RegExp('for each in (.+); do').firstMatch(steps)![1]!))
match[1]!: match[2]!,
};
test("makes each sidecar's user with the ID its container runs as", () {
const runs = {
'jellyfin': 'compose.jellyfin.yaml',
'languagetool': 'compose.languagetool.yaml',
'brouter': 'compose.train.yaml',
'maptiles': 'compose.maps.yaml',
'photon': 'compose.search.yaml',
};
expect(users.keys, unorderedEquals(runs.keys));
for (final MapEntry(key: name, value: file) in runs.entries) {
final id = users[name]!;
expect(compose(file), contains('user: "$id:$id"'), reason: name);
}
});
test("never gives a sidecar the runner's ID, nor one ID twice", () {
expect(users.values, isNot(contains('61000')));
expect(users.values.toSet(), hasLength(users.length));
});
test("makes each sidecar's folder its own, where its container mounts "
'it', () {
const folders = {
'languagetool': (r'$d/languagetool/fasttext', './languagetool/fasttext'),
'brouter': (r'$d/brouter/segments', './brouter/segments'),
'maptiles': (r'$d/maps/tiles', './maps/tiles'),
'photon': (r'$d/photon/data', './photon/data'),
};
final mounted = [
for (final file in [
'compose.languagetool.yaml',
'compose.train.yaml',
'compose.maps.yaml',
'compose.search.yaml',
])
compose(file),
].join('\n');
for (final MapEntry(key: owner, value: (made, mount)) in folders.entries) {
expect(
RegExp('sudo install -d -o $owner -g $owner .*${RegExp.escape(made)}')
.hasMatch(steps),
isTrue,
reason: made,
);
expect(mounted, contains(mount), reason: mount);
}
});
test("gives each app's server a role of its own, owning its database "
'alone, and lets no role connect to a database not its own', () {
for (final app in ['train', 'todo', 'meditate']) {
expect(
steps,
contains("CREATE ROLE halkyon_$app LOGIN PASSWORD :'$app';"),
reason: app,
);
expect(
steps,
contains('CREATE DATABASE halkyon_$app OWNER halkyon_$app;'),
reason: app,
);
expect(
compose('compose.$app.yaml'),
contains(
r'${'
'${app.toUpperCase()}_POSTGRES_PASSWORD',
),
reason: app,
);
}
for (final app in ['learn', 'train', 'todo', 'meditate']) {
expect(
steps,
contains('REVOKE CONNECT ON DATABASE halkyon_$app FROM PUBLIC;'),
reason: app,
);
}
expect(steps, isNot(contains('SUPERUSER')));
expect(steps, isNot(contains('CREATEDB')));
});
test("pins the developer's key on deploy to redeploy.sh alone", () {
expect(
steps,
contains('restrict,command="/home/deploy/halkyon/deploy/redeploy.sh" %s'),
);
});
}