devenv.nix
1{ pkgs, config, ... }:
2
3# devkit (imported in devenv.yaml) already provides bug, secrets,
4# local-code-review, jujutsu, and the customized interactive bash environment,
5# so don't repeat those here.
6#
7# Seeing the appview in development:
8#
9# devenv up -d # bridge, runner and appview on 127.0.0.1:8475-8477
10# devenv tasks run ci:seed # a demo forge repo; pushing to it starts a gated run
11# open http://127.0.0.1:8477 # password: dev
12#
13# Everything lives in .devenv/state/ci-dev (delete it to start over).
14let
15 dev = "${config.devenv.state}/ci-dev";
16 bin = "${dev}/bin/ci";
17in
18{
19 languages.go.enable = true;
20
21 packages = [
22 pkgs.golangci-lint
23 pkgs.just
24 pkgs.jq
25 pkgs.git
26 pkgs.mkpasswd
27 pkgs.util-linux # flock
28 ];
29
30 # Builds the ci binary (and its ci-result alias) into the dev state dir.
31 # The three processes start together, so builds are serialized.
32 scripts.ci-dev-build.exec = ''
33 set -euo pipefail
34 mkdir -p ${dev}/bin
35 (
36 flock 9
37 cd "$DEVENV_ROOT"
38 go build -o ${dev}/bin/ci ./cmd/ci
39 ln -sf ci ${dev}/bin/ci-result
40 ) 9>${dev}/build.lock
41 '';
42
43 # Dev credentials: the appview password is "dev", the runner token and an
44 # agent token ("claude") are fixed strings. Never use these anywhere real.
45 scripts.ci-dev-secrets.exec = ''
46 set -euo pipefail
47 mkdir -p ${dev}
48 [ -f ${dev}/runner-token ] || echo dev-runner-token > ${dev}/runner-token
49 [ -f ${dev}/agent-tokens ] || echo "claude dev-agent-token-0123456789" > ${dev}/agent-tokens
50 [ -f ${dev}/password-hash ] || mkpasswd -m bcrypt -R 5 dev > ${dev}/password-hash
51 '';
52
53 processes.bridge.exec = ''
54 ci-dev-build
55 exec ${bin} bridge serve --listen 127.0.0.1:8475 \
56 --db ${dev}/bridge.db --spool ${dev}/spool --forge-host git.dev
57 '';
58
59 # Actions run with plain bash (no devenv) and as child processes: this is a
60 # UI playground, not the sandboxed production setup.
61 processes.runner.exec = ''
62 ci-dev-build
63 ci-dev-secrets
64 exec ${bin} runner serve --listen 127.0.0.1:8476 \
65 --state-dir ${dev}/runner \
66 --bridge ws://127.0.0.1:8475/events \
67 --knot git.dev --clone-url '${dev}/forge/{repo}.git' \
68 --launcher local --shell bash \
69 --token-file ${dev}/runner-token
70 '';
71
72 processes.appview.exec = ''
73 ci-dev-build
74 ci-dev-secrets
75 exec ${bin} appview serve --listen 127.0.0.1:8477 \
76 --runner http://127.0.0.1:8476 --runner-token-file ${dev}/runner-token \
77 --password-hash-file ${dev}/password-hash \
78 --agent-tokens-file ${dev}/agent-tokens \
79 --knot git.dev --secure-cookies=false
80 '';
81
82 # A demo repo on a "forge" that is a directory. Each run of this task commits
83 # to it and pushes; the repo's post-receive hook hands the push to the
84 # bridge, the runner starts the demo workflow, and the run waits at its
85 # "Deploy demo?" gate. Run it again for another run.
86 scripts.ci-dev-seed.exec = ''
87 set -euo pipefail
88 ci-dev-build
89 ci-dev-secrets
90 forge=${dev}/forge/demo.git
91 work=${dev}/work/demo
92 if [ ! -d "$forge" ]; then
93 git init -q --bare -b main "$forge"
94 cat > "$forge/hooks/post-receive" <<HOOK
95 #!/bin/sh
96 exec ${bin} bridge notify --spool ${dev}/spool --server http://127.0.0.1:8475 --repo demo
97 HOOK
98 chmod +x "$forge/hooks/post-receive"
99 fi
100 if [ ! -d "$work/.git" ]; then
101 mkdir -p "$work"
102 git init -q -b main "$work"
103 git -C "$work" remote add origin "$forge"
104 fi
105 mkdir -p "$work/.tangled/workflows"
106 cat > "$work/.tangled/workflows/deploy.yml" <<'WF'
107 # Demo workflow for the dev appview.
108 when: [{ event: [push, manual], branch: main }]
109 engine: devenv
110 steps: [test, build, deploy]
111 actions:
112 test:
113 command: |
114 for i in 1 2 3; do echo "testing $i"; sleep 1; done
115 echo "something noisy" >&2
116 printf '%s\n' '{"suite":"demo","passed":3}' > "$CI_ARTIFACTS/report.json"
117 ci-result ok --summary "3 tests passed" --attach "report=report.json"
118 build:
119 command: |
120 echo "building $CI_SHA"
121 echo built > "$CI_STATE/artifact"
122 deploy:
123 command: echo "deploying $(cat "$CI_STATE/artifact")"
124 gate: { prompt: "Deploy demo?" }
125 rollback:
126 command: echo "rolling back $(cat "$CI_STATE/artifact")"
127 manual: true
128 style: danger
129 label: Roll back
130 gate: { prompt: "Roll the demo back?" }
131 WF
132 n=$(( $(cat "$work/counter" 2>/dev/null || echo 0) + 1 ))
133 echo "$n" > "$work/counter"
134 git -C "$work" add -A
135 git -C "$work" -c user.name=dev -c user.email=dev@example.com commit -q -m "demo change $n"
136 git -C "$work" push -q origin main
137 echo "pushed demo change $n; open http://127.0.0.1:8477 (password: dev)"
138 '';
139
140 tasks."ci:seed".exec = "ci-dev-seed";
141}