-
-
Notifications
You must be signed in to change notification settings - Fork 36
215 lines (194 loc) · 8.53 KB
/
Copy pathci.yml
File metadata and controls
215 lines (194 loc) · 8.53 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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
name: CI
on:
push:
branches: [master]
tags: ['[0-9]+.[0-9]+.[0-9]+']
pull_request:
branches: [master]
# Least privilege by default; only `publish` widens this.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 26
cache: npm
- run: npm ci
- run: npm run lint
- run: npx tsc --noEmit
# Starts a MongoDB testcontainer, so this is slower than a pure unit suite.
- run: npm test
image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
# load: true puts the image in the runner's local daemon so it can be `docker run`.
- uses: docker/build-push-action@v7
with:
context: .
load: true
tags: swgohbot:ci
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Node version matches engines
run: docker run --rm swgohbot:ci node --version | grep -q '^v26\.'
# The runtime reads these from data/; a broken .dockerignore would ship an image that boots
# and then fails on the first command.
- name: Runtime data files are present
run: docker run --rm swgohbot:ci sh -c 'test -s data/characters.json && test -s data/ships.json && test -s data/factionNames.json && test -s /app/data-dist/characters.json'
# The regression test for the clone-free deploy: an empty bind mount must end up populated,
# because a bind mount never inherits image content the way a named volume does.
- name: Seeding populates an empty data directory
run: |
SEED_TMP=$(mktemp -d)
docker run --rm --user root -v "$SEED_TMP:/app/data" swgohbot:ci node scripts/seedData.ts
test -s "$SEED_TMP/characters.json"
test -s "$SEED_TMP/factionNames.json"
test -s "$SEED_TMP/acronyms.json"
# These are read only by dataUpdater, which stays on the host, and must not be in the image.
- name: dataUpdater-only directories are absent
run: |
if docker run --rm swgohbot:ci sh -c 'test -e data/gameDataFiles -o -e data/swgoh-json-files'; then
echo "dataUpdater-only files leaked into the image" >&2
exit 1
fi
# No .env exists in the image or on the runner. Starting purely from environment variables is
# the property that makes this deployable at all, so it is asserted rather than assumed.
- name: swapiServe starts and serves from environment alone
run: |
docker run -d --name swapiserve-ci --network host \
-e SWAPI_SERVE_HOST=0.0.0.0 \
-e SWAPI_SERVE_PORT=3899 \
-e SWAPI_CLIENT_URL=http://localhost:9998 \
-e SWAPI_STATCALC_URL=http://localhost:9997 \
-e SWAPI_ACCESS_KEY=ci \
-e SWAPI_SECRET_KEY=ci \
-e MONGODB_URL=mongodb://localhost:27017 \
-e DISCORD_TOKEN=ci \
-e DISCORD_CLIENT_ID=0 \
-e DISCORD_OWNER_ID=0 \
swgohbot:ci node services/swapiServe/index.ts
for i in $(seq 1 30); do
if curl -fsS localhost:3899/status >/dev/null; then echo "serving"; exit 0; fi
sleep 2
done
echo "swapiServe never served /status" >&2
docker logs swapiserve-ci
exit 1
# The bind is the control API's only guard when no secret is set, so an exposed bind must say
# so. A silent start here would mean the warning has been lost.
- name: swapiServe warns when the control API is unauthenticated and exposed
run: docker logs swapiserve-ci 2>&1 | grep -q 'SWAPI_SERVE_CONTROL_SECRET'
# The shard manager itself cannot be smoke-tested: Manager.spawn fails against a dummy token
# and the process exits, racing the probe. So the status surface is started directly, which
# is what this needs to prove anyway - that the module resolves inside the image (nothing in
# .dockerignore lost it) and binds from environment alone.
#
# curl -fsS requires a 2xx, so this also pins the startup rule: /health must answer 200 while
# the fleet is starting. If that ever regressed to 503, every deploy would go unhealthy for
# the length of the spawn.
- name: shardStatus starts and serves from environment alone
run: |
docker run -d --name shardstatus-ci --network host \
-e SHARD_STATUS_HOST=0.0.0.0 \
-e SHARD_STATUS_PORT=3898 \
-e SWAPI_CLIENT_URL=http://localhost:9998 \
-e SWAPI_STATCALC_URL=http://localhost:9997 \
-e SWAPI_ACCESS_KEY=ci \
-e SWAPI_SECRET_KEY=ci \
-e MONGODB_URL=mongodb://localhost:27017 \
-e DISCORD_TOKEN=ci \
-e DISCORD_CLIENT_ID=0 \
-e DISCORD_OWNER_ID=0 \
swgohbot:ci node --input-type=module -e "
import { ShardRegistry } from '/app/modules/shardStatus/registry.ts';
import { startShardStatusServer } from '/app/modules/shardStatus/server.ts';
import { systemClock } from '/app/services/swapiServe/clock.ts';
const registry = new ShardRegistry(systemClock);
registry.registerShard(0);
await startShardStatusServer(registry, {
port: Number(process.env.SHARD_STATUS_PORT),
host: process.env.SHARD_STATUS_HOST,
});
"
for i in $(seq 1 30); do
if curl -fsS localhost:3898/health | grep -q '"state":"starting"'; then echo "serving"; exit 0; fi
sleep 2
done
echo "shardStatus never served /health" >&2
docker logs shardstatus-ci
exit 1
# eventServe's init() connects to MongoDB before it listens, so it needs one to start.
- name: Start MongoDB for eventServe
run: docker run -d --name mongo-ci --network host mongo:8
- name: eventServe answers /health without a bearer
run: |
docker run -d --name eventserve-ci --network host \
-e MONGODB_URL=mongodb://localhost:27017 \
-e EVENT_SERVER_PORT=3799 \
-e EVENT_SERVER_SECRET=ci-secret \
-e SWAPI_CLIENT_URL=http://localhost:9998 \
-e SWAPI_STATCALC_URL=http://localhost:9997 \
-e SWAPI_ACCESS_KEY=ci \
-e SWAPI_SECRET_KEY=ci \
-e DISCORD_TOKEN=ci \
-e DISCORD_CLIENT_ID=0 \
-e DISCORD_OWNER_ID=0 \
swgohbot:ci node services/eventServe.ts
for i in $(seq 1 30); do
if curl -fsS localhost:3799/health | grep -q '"status":"ok"'; then echo "healthy"; exit 0; fi
sleep 2
done
echo "eventServe never answered /health" >&2
docker logs eventserve-ci
exit 1
# /health must not require the bearer, or the compose healthcheck would need to carry it.
# EVENT_SERVER_SECRET is set above precisely so this proves the guards still apply elsewhere.
- name: eventServe still rejects unauthenticated POSTs
run: |
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST localhost:3799/checkEvents)
test "$code" = "401" || { echo "expected 401, got $code" >&2; exit 1; }
- name: Container logs on failure
if: failure()
run: |
docker logs swapiserve-ci || true
docker logs shardstatus-ci || true
docker logs eventserve-ci || true
publish:
if: startsWith(github.ref, 'refs/tags/')
needs: [check, image]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/jmiln/swgohbot
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
# Reuses the layers the image job just built, via the shared gha cache.
- uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha