-
Notifications
You must be signed in to change notification settings - Fork 1
126 lines (115 loc) · 5.24 KB
/
Copy pathdeploy.yaml
File metadata and controls
126 lines (115 loc) · 5.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
name: deploy
# Self-deciding deploy:
# * No VM tagged `computer` exists -> CREATE one from the freshly-built image
# (bootstrap / disaster recovery). A fresh VM has no state, so this is the ONE
# path that needs the backup secrets — passed as `exe.dev new --env` so the box
# can restore from B2 on boot. (Brand-new setups still need the manual seeding
# in README: tailscale authkey, pilegram env.)
# * A VM tagged `computer` exists -> ACTIVATE IN PLACE over Tailscale. No VM
# recreate, no node churn, no restore, and NO secrets in CI — RESTIC/B2/pilegram
# already live on the box.
# The next deploy after a create finds the VM and switches it onto the generation.
on:
workflow_dispatch:
inputs:
image_tag:
description: image tag to create from (blank = the built commit's short SHA)
default: ""
workflow_run:
workflows: [build]
types: [completed]
branches: [master]
concurrency:
group: deploy
cancel-in-progress: false
env:
VM_NAME: computer
VM_TAG: computer
jobs:
deploy:
name: Create-or-activate
runs-on: ubuntu-24.04
if: >-
github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success'
steps:
- uses: actions/checkout@v5
with:
# the commit that was built (workflow_run), else the dispatched ref.
# depth 1 is enough: create uses `git rev-parse HEAD`, activate builds
# from the working tree — neither needs history.
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Configure exe.dev SSH
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
# Pin exe.dev by its documented host-key fingerprint (faq/host-key).
ssh-keyscan exe.dev > ~/.ssh/known_hosts 2>/dev/null
ssh-keygen -lf ~/.ssh/known_hosts \
| grep -q "SHA256:JJOP/lwiBGOMilfONPWZCXUrfK154cnJFXcqlsi6lPo" \
|| { echo "exe.dev host-key fingerprint mismatch"; exit 1; }
- name: Is there a computer VM?
id: check
run: |
if ! ssh -i ~/.ssh/id_ed25519 exe.dev ls --l --json > vms.json; then
echo "exe.dev VM listing failed"
exit 1
fi
if ! jq -e . vms.json >/dev/null; then
echo "exe.dev VM listing did not return JSON; raw output follows:"
sed -n '1,80p' vms.json
exit 1
fi
n=$(jq -r --arg tag "$VM_TAG" '[.vms[] | select((.tags // []) | index($tag))] | length' vms.json)
echo "found $n VM(s) tagged $VM_TAG"
if [ "$n" -gt 0 ]; then echo "exists=true"; else echo "exists=false"; fi >> "$GITHUB_OUTPUT"
# ---- bootstrap: no VM yet -> create one (backup secrets from CI) ----------
- name: Create the computer VM
if: steps.check.outputs.exists == 'false'
env:
IN_TAG: ${{ github.event.inputs.image_tag }}
RESTIC_PASSWORD: ${{ secrets.RESTIC_PASSWORD }}
B2_ACCOUNT_KEY: ${{ secrets.B2_ACCOUNT_KEY }}
run: |
tag="$IN_TAG"
[ -n "$tag" ] || tag="$(git rev-parse --short HEAD)"
echo "creating $VM_NAME from ghcr.io/ngalaiko/computer.exe:$tag"
ssh -i ~/.ssh/id_ed25519 exe.dev new \
--image="ghcr.io/ngalaiko/computer.exe:$tag" \
--name "$VM_NAME" \
--tag "$VM_TAG" \
--env RESTIC_REPOSITORY=b2:ngalaiko-backups:exedev \
--env RESTIC_PASSWORD="$RESTIC_PASSWORD" \
--env B2_ACCOUNT_ID=0036cfed748e70c0000000002 \
--env B2_ACCOUNT_KEY="$B2_ACCOUNT_KEY"
# 8080 = services.ingress.publicPort, the public root port.
ssh -i ~/.ssh/id_ed25519 exe.dev share port "$VM_NAME" 8080
ssh -i ~/.ssh/id_ed25519 exe.dev share set-public "$VM_NAME"
# ---- steady state: VM exists -> activate the new generation in place ------
- uses: nixbuild/nix-quick-install-action@v35
if: steps.check.outputs.exists == 'true'
# The box is reachable only over its tailnet. This tailnet OAuth client is
# the only credential the in-place path needs — NOT an app secret. The client
# id is public (inline); only the secret is a repo secret. The policy must let
# tag:ci tailscale-ssh nikita@ the computer node (README):
# "ssh": [{ "action": "accept", "src": ["tag:ci"],
# "dst": ["tag:computer"], "users": ["nikita"] }]
- name: Join tailnet
if: steps.check.outputs.exists == 'true'
uses: tailscale/github-action@v3
with:
oauth-client-id: kt74FE1o8p11CNTRL
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
# DEPLOY_FLAKE points at the built commit on GitHub, so the box fetches +
# builds the generation itself — nothing is pushed from the runner (which
# the box would reject as unsigned). `.#deploy` is still the local app.
- name: Activate in place
if: steps.check.outputs.exists == 'true'
env:
DEPLOY_FLAKE: github:ngalaiko/computer/${{ github.event.workflow_run.head_sha || github.sha }}
run: nix run .#deploy