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}