-
-
Notifications
You must be signed in to change notification settings - Fork 2
330 lines (294 loc) · 12.4 KB
/
Copy pathci.yml
File metadata and controls
330 lines (294 loc) · 12.4 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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
name: CI
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fmt:
name: gofmt
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
# CLI-R01: this gate did not exist before 2026-08-23, which is how 30
# unformatted files reached main while the PRI claimed `gofmt -l .` was
# enforced. scripts/ci/gofmt-check.sh is the single source of the rule so
# the local `make fmt-check` and this job can never disagree.
- name: gofmt check
run: bash scripts/ci/gofmt-check.sh
# A duplicate mapping key (two `if:` on one step) parses fine in most YAML
# tooling but GitHub rejects the whole file, reporting only "this run
# likely failed because of a workflow file issue". Catch it here instead
# of burning a push cycle.
- name: workflow YAML lint
run: bash scripts/ci/workflow-lint.sh
golangci-lint:
# P6-E2-W1-S1-T4: the PRI's Dev Env Quick Ref lint contract lists
# `golangci-lint run` alongside gofmt/vet, but no CI job ever enforced it.
# Pinned to a released tag (never @latest), matching the repo's existing
# tool-pinning discipline (see 2451d9bd "pin govulncheck and go-licenses
# instead of installing @latest").
name: golangci-lint
runs-on: ubuntu-latest
timeout-minutes: 10
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v9.3.0
with:
version: v2.13.2
args: --timeout=10m
ci:
name: Vet, Build & Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
# 10-min job timeout was too tight for cold-cache Windows / macOS runs:
# windows-2022 + macOS spend ~2-3 minutes on vet alone (unlike Linux ~15s)
# and 5-7 minutes on -short tests when Go module/build cache is empty.
# 20 minutes gives cold-cache headroom; warm-cache passes typically finish
# in under 5 minutes across all OSes.
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- ubuntu-24.04-arm # linux/arm64 — first execution test before ship
- macos-15-intel # macOS Intel (darwin/amd64) — macos-13 retired Dec 2025
- macos-14 # Apple Silicon (darwin/arm64)
- windows-2022 # Windows — pure-Go only; Docker tests skipped
include:
# Windows: skip Docker-dependent integration tests.
# Docker Desktop on windows-2022 runners is unavailable by default.
# Covered by WSL2 docs (S49-T04). Native Windows binary deferred to v1.1.0.
- os: windows-2022
skip_docker_tests: "true"
env:
CGO_ENABLED: 0
# Defense-in-depth alongside the cmd/commands TestMain guard: `nself
# start`'s zero-config AI auto-install must never fire a real network
# install during CI's unit-test run. Same guard already used by
# e2e-golden-path.yml and rls-pentest-smoke.yml for the same reason.
AI_AUTO_INSTALL: 'false'
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Cache Go modules
uses: actions/cache@v6
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.mod') }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Vet
if: matrix.skip_docker_tests != 'true'
run: go vet -mod=vendor ./...
- name: Vet (Windows — pure-Go packages only)
if: matrix.skip_docker_tests == 'true'
run: go vet -mod=vendor ./...
shell: cmd
- name: Build
if: matrix.skip_docker_tests != 'true'
run: go build -mod=vendor ./...
- name: Build (Windows)
if: matrix.skip_docker_tests == 'true'
run: go build -mod=vendor ./...
shell: cmd
- name: Test
if: matrix.skip_docker_tests != 'true'
run: go test -mod=vendor -timeout 10m ./...
- name: Test (Windows — pure-Go packages; Docker integration tests skipped)
if: matrix.skip_docker_tests == 'true'
# Docker integration tests require Docker Desktop which is not available
# on windows-2022 GitHub-hosted runners. Only pure-Go packages run here.
# -timeout=240s (4 min) ensures a hung test panics with a goroutine dump
# instead of silently filling the 20-min job cap.
run: go test -mod=vendor -short -timeout=240s ./...
shell: cmd
# S49-T06: Apple Silicon (darwin/arm64) smoke test.
# The darwin/arm64 binary is cross-compiled on ubuntu-latest but never tested natively
# before this job was added. This job runs on macos-14 (Apple Silicon runner) and
# executes the binary smoke sequence: version + doctor + init + build.
#
# nself start is intentionally NOT run — Docker on macOS runners is unreliable.
# build generates docker-compose.yml without running containers.
apple-silicon-smoke:
name: Apple Silicon smoke (darwin/arm64)
runs-on: macos-14
timeout-minutes: 10
env:
CGO_ENABLED: 0
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Go
uses: actions/setup-go@v7
with:
go-version-file: go.mod
- name: Cache Go modules
uses: actions/cache@v6
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('go.mod') }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build darwin/arm64 binary
env:
GOOS: darwin
GOARCH: arm64
run: |
go build -mod=vendor -ldflags='-s -w' -o ./nself ./cmd/nself/
echo "PASS: darwin/arm64 build succeeded"
- name: Verify binary is native arm64
run: |
file ./nself
file ./nself | grep -q "arm64" || {
echo "FAIL: binary is not arm64"
exit 1
}
echo "PASS: binary is native darwin/arm64"
- name: Smoke — nself version
run: |
./nself version
echo "PASS: nself version"
- name: Smoke — nself doctor
run: |
# Doctor checks Docker + system dependencies.
# Docker is not available on macos-14 runners — skip Docker checks.
# Verify the command exits cleanly (even with Docker missing).
./nself doctor || true
echo "INFO: nself doctor ran (Docker may not be available on runner)"
- name: Smoke — nself init
run: |
TMPDIR=$(mktemp -d)
# CLI v1.0.9+: the init command takes --name (not --project-name) and
# writes into the current working directory (there is no --dir flag).
NSELF_BIN="$(pwd)/nself"
cd "$TMPDIR"
"$NSELF_BIN" init --name smoke-test --non-interactive || {
echo "FAIL: nself init failed"
exit 1
}
echo "PASS: nself init succeeded in $TMPDIR"
- name: Smoke — nself build
run: |
TMPDIR=$(mktemp -d)
NSELF_BIN="$(pwd)/nself"
cd "$TMPDIR"
"$NSELF_BIN" init --name smoke-build --non-interactive
# build generates docker-compose.yml without starting containers
"$NSELF_BIN" build || {
echo "FAIL: nself build failed"
exit 1
}
echo "PASS: nself build succeeded"
# nself start intentionally NOT run — Docker on macos-14 is unreliable
# UA-07: Meta-monitoring and dispatch-dashboard steps.
# These steps require three org secrets that are NOT provisioned by default:
# GITHUB_APP_PRIVATE_KEY — GitHub App private key for installation token auth
# SLACK_WEBHOOK — Incoming Webhook URL for build-status notifications
# NSELF_DISPATCH_APP_ID — GitHub App ID for repository_dispatch triggers
#
# Each step uses `if: ${{ secrets.X != '' }}` so absent secrets produce a clean
# skip rather than a workflow failure. See .github/wiki/USER-ACTION-QUEUE.md § UA-07
# for provisioning instructions.
dispatch-monitor:
name: Dispatch monitor (skipped when secrets absent)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Notify build status via Slack
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
# Skip gracefully when secret is absent (UA-07: provision SLACK_WEBHOOK to enable)
if [ -z "${SLACK_WEBHOOK}" ]; then
echo "INFO: SLACK_WEBHOOK not configured — skipping (see UA-07)"
exit 0
fi
PAYLOAD="{\"text\":\"CLI CI passed on \`${GITHUB_REF_NAME}\` (${GITHUB_SHA:0:7}).\",\"username\":\"nself-ci\"}"
curl -fsS -X POST -H 'Content-type: application/json' \
--data "$PAYLOAD" "$SLACK_WEBHOOK"
echo "INFO: Slack notification sent"
- name: Generate GitHub App installation token
id: app-token
env:
APP_ID: ${{ secrets.NSELF_DISPATCH_APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
run: |
# Skip gracefully when secrets are absent (UA-07: provision both secrets to enable)
if [ -z "${APP_ID}" ] || [ -z "${APP_PRIVATE_KEY}" ]; then
echo "INFO: NSELF_DISPATCH_APP_ID or GITHUB_APP_PRIVATE_KEY not configured — skipping (see UA-07)"
echo "token=" >> "$GITHUB_OUTPUT"
exit 0
fi
# Generate a short-lived JWT for the GitHub App, then exchange it for
# an installation token scoped to this repository.
NOW=$(date +%s)
EXP=$((NOW + 540))
HEADER=$(printf '{"alg":"RS256","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
PAYLOAD_JWT=$(printf '{"iat":%d,"exp":%d,"iss":"%s"}' "$NOW" "$EXP" "$APP_ID" \
| base64 -w0 | tr '+/' '-_' | tr -d '=')
UNSIGNED="${HEADER}.${PAYLOAD_JWT}"
SIG=$(printf '%s' "$UNSIGNED" \
| openssl dgst -sha256 -sign <(printf '%s' "$APP_PRIVATE_KEY") \
| base64 -w0 | tr '+/' '-_' | tr -d '=')
JWT="${UNSIGNED}.${SIG}"
INSTALL_ID=$(curl -fsS -H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations" \
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
TOKEN=$(curl -fsS -X POST -H "Authorization: Bearer $JWT" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/app/installations/${INSTALL_ID}/access_tokens" \
| grep -o '"token":"[^"]*"' | cut -d'"' -f4)
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
echo "INFO: installation token generated"
- name: Trigger downstream dispatch (plugins registry sync)
env:
TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# Skip gracefully when app token was not generated (UA-07: provision secrets to enable)
if [ -z "${TOKEN}" ]; then
echo "INFO: App token not available — skipping dispatch (see UA-07)"
exit 0
fi
# repository_dispatch to nself-org/plugins triggers the registry sync
# workflow when CLI ships a new release. Skipped on PRs (only runs on main).
if [ "${GITHUB_EVENT_NAME}" != "push" ] || [ "${GITHUB_REF}" != "refs/heads/main" ]; then
echo "INFO: Not a main-branch push — skipping dispatch"
exit 0
fi
curl -fsS -X POST \
-H "Authorization: token $TOKEN" \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/json" \
"https://api.github.com/repos/nself-org/plugins/dispatches" \
--data '{"event_type":"cli-release","client_payload":{"sha":"'"${GITHUB_SHA}"'"}}'
echo "INFO: repository_dispatch sent to nself-org/plugins"