|
1 | 1 | { |
2 | 2 | "id": "20e26efc", |
3 | | - "title": "CI job: run setup + tests on Ubuntu droplet per PR", |
| 3 | + "title": "CI job: run setup + tests on fresh Ubuntu droplet per PR", |
4 | 4 | "tags": [ |
5 | 5 | "infra", |
6 | 6 | "ci", |
7 | 7 | "ubuntu" |
8 | 8 | ], |
9 | 9 | "status": "todo", |
10 | | - "created_at": "2026-02-17T02:30:52.375Z" |
| 10 | + "created_at": "2026-02-17T02:30:52.375Z", |
| 11 | + "assigned_to_session": "381813d9-c69a-4472-9a00-e232ffb746d1" |
11 | 12 | } |
12 | 13 |
|
13 | 14 | ## Goal |
14 | | -Add a GitHub Actions workflow that SSHes into the DigitalOcean droplet and runs the full hornet setup + test suite on every PR. |
| 15 | +Add a GitHub Actions workflow that spins up a fresh DigitalOcean droplet, runs the full hornet setup + test suite, and destroys it — on every PR. |
15 | 16 |
|
16 | | -## Depends on |
17 | | -- TODO-cb931656 (manual verification must pass first) |
| 17 | +## Verified approach |
| 18 | +Tested manually — full destroy/create/setup/test cycle works end-to-end in ~2 minutes. |
18 | 19 |
|
19 | | -## Design decisions needed |
20 | | -- **Fresh state per run**: `uninstall.sh` at start of each run? Or snapshot/restore? Or ephemeral droplet via DO API? |
21 | | -- **Secrets**: droplet IP + SSH key stored as GitHub Actions secrets (`DROPLET_IP`, `DROPLET_SSH_KEY`) |
22 | | -- **Concurrency**: only one CI run at a time on the droplet (use GitHub concurrency group) |
23 | | -- **Scope**: full setup + test, or just test.sh (setup is slow, ~2-3 min)? |
| 20 | +| Step | Method | Time | |
| 21 | +|------|--------|------| |
| 22 | +| Fresh env | Create droplet via DO API | ~45s | |
| 23 | +| Wait for SSH | Poll until port 22 responds | ~15s | |
| 24 | +| Prereqs | `apt-get install git curl tmux iptables docker.io` (wait for unattended-upgrades lock first) | ~20s | |
| 25 | +| Source | `tar + scp + extract + git init` | ~5s | |
| 26 | +| Setup | `setup.sh hornet_admin` | ~60s | |
| 27 | +| Tests | `npm install + bin/test.sh` | ~15s | |
| 28 | +| Cleanup | Destroy droplet + delete SSH key from DO (always, even on failure) | instant | |
24 | 29 |
|
25 | | -## Proposed workflow |
| 30 | +## GitHub Actions secrets needed |
| 31 | +- `DO_API_TOKEN` — DigitalOcean API token |
| 32 | + |
| 33 | +That's it. SSH keys are **ephemeral per run**: |
| 34 | +1. `ssh-keygen` a throwaway ed25519 keypair on the runner |
| 35 | +2. Register pubkey with DO API → get `ssh_key_id` |
| 36 | +3. Create droplet with that `ssh_key_id` |
| 37 | +4. Use the private key to SSH in |
| 38 | +5. Cleanup: destroy droplet AND delete SSH key from DO account |
| 39 | + |
| 40 | +No persistent SSH keys stored anywhere. |
| 41 | + |
| 42 | +## Design |
| 43 | +- **Ephemeral everything**: fresh droplet, fresh SSH key, destroyed after. Zero state between runs. |
| 44 | +- **Concurrency group**: only one integration run at a time (avoid parallel droplets piling up). |
| 45 | +- **Always cleanup**: use `if: always()` so droplet + SSH key are destroyed even if tests fail. |
| 46 | +- **Region/size**: `tor1`, `s-2vcpu-4gb` (~$0.003/run at ~2 min). |
| 47 | +- **Unattended-upgrades**: fresh Ubuntu runs apt on first boot — CI must wait for the dpkg lock. |
| 48 | + |
| 49 | +## Steps |
| 50 | +1. Add `DO_API_TOKEN` secret to GitHub repo via `gh secret set` |
| 51 | +2. Write `.github/workflows/integration.yml` |
| 52 | +3. Test on a real PR |
| 53 | +4. Optionally add status badge to README |
| 54 | + |
| 55 | +## Workflow skeleton |
26 | 56 | ```yaml |
27 | 57 | name: Integration (Ubuntu) |
28 | 58 | on: |
29 | 59 | pull_request: |
30 | 60 | branches: [main] |
31 | 61 |
|
32 | 62 | concurrency: |
33 | | - group: droplet-integration |
| 63 | + group: integration-ubuntu |
34 | 64 | cancel-in-progress: true |
35 | 65 |
|
36 | 66 | jobs: |
37 | 67 | integration: |
38 | 68 | runs-on: ubuntu-latest |
39 | 69 | steps: |
40 | 70 | - uses: actions/checkout@v4 |
41 | | - - name: Run on droplet |
42 | | - env: |
43 | | - DROPLET_IP: ${{ secrets.DROPLET_IP }} |
44 | | - SSH_KEY: ${{ secrets.DROPLET_SSH_KEY }} |
| 71 | + - name: Generate ephemeral SSH key |
| 72 | + run: | |
| 73 | + ssh-keygen -t ed25519 -f ~/.ssh/ci_key -N "" -q |
| 74 | + # Register with DO, save key ID |
| 75 | + - name: Create droplet |
| 76 | + # POST /v2/droplets with ssh_key_id, poll until active, extract IP |
| 77 | + - name: Wait for SSH |
| 78 | + # Loop ssh -o ConnectTimeout=5 until success |
| 79 | + - name: Install prereqs |
| 80 | + # Wait for apt lock, then apt-get install |
| 81 | + - name: Upload source |
| 82 | + # tar + scp |
| 83 | + - name: Run setup |
| 84 | + # setup.sh hornet_admin |
| 85 | + - name: Run tests |
| 86 | + # npm install + bin/test.sh |
| 87 | + - name: Cleanup |
| 88 | + if: always() |
45 | 89 | run: | |
46 | | - # SSH into droplet, rsync repo, run uninstall (clean slate), |
47 | | - # run setup.sh, deploy.sh, test.sh, security-audit.sh |
| 90 | + # DELETE /v2/droplets/$DROPLET_ID |
| 91 | + # DELETE /v2/account/keys/$SSH_KEY_ID |
48 | 92 | ``` |
49 | | -
|
50 | | -## Steps |
51 | | -1. Create SSH key pair for CI, add public key to droplet |
52 | | -2. Add `DROPLET_IP` and `DROPLET_SSH_KEY` as GitHub repo secrets |
53 | | -3. Write the workflow file (`.github/workflows/integration.yml`) |
54 | | -4. Handle cleanup: uninstall.sh at start of run for clean state |
55 | | -5. Fail the PR if any step exits non-zero |
56 | | -6. Consider: also run security-audit.sh (some checks need live system) |
57 | | - |
58 | | -## Open questions |
59 | | -- Do we want to spin up/destroy droplets per run (more isolated, costs more) or reuse one? |
60 | | -- Should we test `start.sh` actually booting an agent, or just setup + unit tests? |
|
0 commit comments