.github/workflows/ci.yml
1
---
2
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
3
name: CI
5
on:
6
push:
7
branches:
8
- main
9
pull_request:
10
branches:
11
- main
12
merge_group:
13
workflow_dispatch:
15
concurrency:
16
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
17
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
19
permissions:
20
contents: read
22
jobs:
23
quality:
24
name: quality
25
runs-on: ubuntu-24.04
26
permissions:
27
contents: read
28
steps:
29
- name: Checkout
30
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
with:
32
persist-credentials: false
34
- name: Setup Mise
35
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
36
with:
37
experimental: true
38
install_args: --locked
40
- name: Cache Go
41
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
42
with:
43
path: |
44
~/.cache/go-build
45
~/go/pkg/mod
46
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
47
restore-keys: ${{ runner.os }}-go-
49
- name: Check formatting
50
run: mise run fmt-check
52
- name: Check module metadata
53
run: mise run tidy-check
55
- name: Run go vet
56
run: mise run vet
58
- name: Check generated files
59
run: mise run generate-check
61
- name: Check annotations
62
run: mise run annotations
64
- name: Check comment policy
65
run: mise run comments
67
- name: Check agent adapters
68
run: mise run agent-policy
70
- name: Lint and audit workflows
71
run: mise run workflow-lint
73
- name: Run govulncheck
74
run: mise run vulncheck
76
lint:
77
name: lint
78
runs-on: ubuntu-24.04
79
permissions:
80
contents: read
81
steps:
82
- name: Checkout
83
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
84
with:
85
persist-credentials: false
87
- name: Setup Mise
88
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
89
with:
90
experimental: true
91
install_args: --locked
93
- name: Cache Go
94
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
95
with:
96
path: |
97
~/.cache/go-build
98
~/go/pkg/mod
99
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
100
restore-keys: ${{ runner.os }}-go-
102
- name: Cache golangci-lint
103
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
104
with:
105
path: ~/.cache/golangci-lint
106
key: ${{ runner.os }}-golangci-lint-${{ hashFiles('go.sum', '.golangci.yml', '.mise/mise.lock') }}
108
- name: Run linter
109
run: mise run lint
111
test:
112
name: test
113
runs-on: ubuntu-24.04
114
permissions:
115
contents: read
116
steps:
117
- name: Checkout
118
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
119
with:
120
persist-credentials: false
122
- name: Setup Mise
123
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
124
with:
125
experimental: true
126
install_args: --locked
128
- name: Cache Go
129
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
130
with:
131
path: |
132
~/.cache/go-build
133
~/go/pkg/mod
134
key: ${{ runner.os }}-go-${{ hashFiles('go.sum') }}
135
restore-keys: ${{ runner.os }}-go-
137
- name: Run tests
138
run: mise run test
140
- name: Report coverage
141
run: go tool cover -func=coverage.out | tail -1
143
container:
144
name: container
145
runs-on: ubuntu-24.04
146
permissions:
147
contents: read
148
steps:
149
- name: Checkout
150
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
151
with:
152
persist-credentials: false
154
- name: Setup Mise
155
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
156
with:
157
experimental: true
158
install: false
160
- name: Resolve Go version
161
id: tools
162
run: echo "go=$(mise config get tools.go)" >> "$GITHUB_OUTPUT"
164
- name: Setup Buildx
165
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
167
- name: Build image
168
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
169
with:
170
context: .
171
push: false
172
load: true
173
tags: koment:ci
174
build-args: GO_VERSION=${{ steps.tools.outputs.go }}
175
cache-from: type=gha
176
cache-to: type=gha,mode=max
178
- name: Run binary
179
run: docker run --rm koment:ci help
181
- name: Exercise unified service
182
env:
183
SYNCHRONIZATION_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184
run: |
185
set -euo pipefail
186
token="$RUNNER_TEMP/koment-synchronization-token"
187
printf '%s' "$SYNCHRONIZATION_TOKEN" > "$token"
188
chmod 0444 "$token"
189
docker run --detach --name koment-ci-service \
190
--publish 127.0.0.1:18080:8080 \
191
--mount type=bind,src="$PWD/testdata/server/repositories.yaml",dst=/config/repositories.yaml,readonly \
192
--mount type=bind,src="$token",dst=/config/synchronization-token,readonly \
193
koment:ci serve --config /config/repositories.yaml \
194
--github-token-file /config/synchronization-token \
195
--listen 0.0.0.0:8080 --trusted-proxies 127.0.0.1/32
196
cleanup() {
197
docker logs koment-ci-service
198
docker rm --force koment-ci-service
199
rm -f "$token"
200
}
201
trap cleanup EXIT
202
for attempt in $(seq 1 30); do
203
if curl --fail --silent http://127.0.0.1:18080/readyz; then
204
break
205
fi
206
if [ "$attempt" -eq 30 ]; then
207
exit 1
208
fi
209
sleep 2
210
done
211
curl --fail --silent http://127.0.0.1:18080/livez
212
test "$(curl --silent --output /dev/null --write-out '%{http_code}' http://127.0.0.1:18080/r/koment/)" = 401
214
- name: Setup Helm
215
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
217
- name: Create Kind cluster
218
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
220
- name: Exercise Helm release
221
env:
222
SYNCHRONIZATION_TOKEN: ${{ secrets.GITHUB_TOKEN }}
223
run: |
224
set -euo pipefail
225
diagnostics() {
226
kubectl get deployments,pods,services --all-namespaces
227
kubectl describe pods --namespace default
228
kubectl logs deployment/koment-koment --namespace default || true
229
}
230
trap diagnostics ERR
231
kind load docker-image koment:ci --name chart-testing
232
kubectl create secret generic koment-synchronization \
233
--from-literal=github-token="$SYNCHRONIZATION_TOKEN"
234
helm upgrade --install koment charts/koment \
235
--set image.repository=koment \
236
--set-string image.tag=ci \
237
--set image.pullPolicy=Never \
238
--set github.existingSecret=koment-synchronization \
239
--wait --timeout 5m
240
helm test koment --logs --timeout 2m
242
setup-action:
243
name: setup-action (${{ matrix.os }})
244
runs-on: ${{ matrix.os }}
245
permissions:
246
contents: read
247
strategy:
248
fail-fast: false
249
matrix:
250
os:
251
- ubuntu-24.04
252
- macos-15
253
steps:
254
- name: Checkout
255
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
256
with:
257
persist-credentials: false
259
- name: Explain why the published release is not installed here
260
if: github.event_name == 'push'
261
run: |
262
echo "::notice::A push to main can be a release commit, whose tag becomes 'latest'" \
263
"minutes before its archives are uploaded. The release workflow verifies its own" \
264
"output once publication finishes."
266
- name: Install koment
267
id: setup
268
if: github.event_name != 'push'
269
uses: ./
271
- name: Verify installation
272
if: github.event_name != 'push'
273
env:
274
INSTALLED_VERSION: ${{ steps.setup.outputs.version }}
275
run: |
276
command -v koment
277
koment version
278
test -n "$INSTALLED_VERSION"
279
koment version | grep -q "$INSTALLED_VERSION"
281
windows-archive:
282
name: windows-archive (advisory)
283
runs-on: windows-2025
284
permissions:
285
contents: read
286
steps:
287
- name: Install the published Windows archive the way Scoop and WinGet do
288
if: github.event_name != 'push'
289
shell: pwsh
290
env:
291
KOMENT_SOURCE: koment-dev/koment
292
run: |
293
$ErrorActionPreference = 'Stop'
295
$resolved = (Invoke-WebRequest -Uri "https://github.com/$env:KOMENT_SOURCE/releases/latest" -UseBasicParsing).BaseResponse.RequestMessage.RequestUri.AbsoluteUri
296
$tag = ($resolved -split '/')[-1]
297
$version = $tag.TrimStart('v')
298
if ($version -notmatch '^\d+\.\d+\.\d+') {
299
throw "could not resolve a koment release from $resolved"
300
}
302
$archive = "koment_${version}_windows_amd64.zip"
303
$base = "https://github.com/$env:KOMENT_SOURCE/releases/download/$tag"
304
$work = New-Item -ItemType Directory -Force -Path (Join-Path $env:RUNNER_TEMP 'koment-windows')
306
Write-Host "installing koment $version (windows/amd64)"
307
Invoke-WebRequest -Uri "$base/$archive" -OutFile (Join-Path $work $archive) -UseBasicParsing
308
Invoke-WebRequest -Uri "$base/koment_${version}_checksums.txt" -OutFile (Join-Path $work 'checksums.txt') -UseBasicParsing
310
$listed = Select-String -Path (Join-Path $work 'checksums.txt') -Pattern " $([regex]::Escape($archive))$"
311
if (-not $listed) {
312
throw "$archive is not listed in the release checksum manifest"
313
}
314
$expected = ($listed.Line -split '\s+')[0]
315
$actual = (Get-FileHash -Path (Join-Path $work $archive) -Algorithm SHA256).Hash.ToLower()
316
if ($expected -ne $actual) {
317
throw "${archive}: expected $expected, got $actual"
318
}
319
Write-Host "${archive}: OK"
321
$extracted = Join-Path $work 'extracted'
322
Expand-Archive -Path (Join-Path $work $archive) -DestinationPath $extracted -Force
323
foreach ($member in @('koment.exe', 'LICENSE', 'README.md')) {
324
if (-not (Test-Path (Join-Path $extracted $member))) {
325
throw "$member is not at the root of $archive"
326
}
327
}
328
Write-Host 'archive root carries koment.exe, LICENSE and README.md'
330
$reported = (& (Join-Path $extracted 'koment.exe') version) -join ' '
331
Write-Host $reported
332
if ($reported -notmatch [regex]::Escape($version)) {
333
throw "koment.exe reported '$reported', which does not name $version"
334
}
336
helm:
337
name: helm
338
runs-on: ubuntu-24.04
339
permissions:
340
contents: read
341
steps:
342
- name: Checkout
343
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
344
with:
345
persist-credentials: false
347
- name: Setup Mise
348
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
349
with:
350
experimental: true
351
install_args: --locked
353
- name: Lint chart
354
run: mise run helm-lint
356
- name: Render supported modes
357
run: mise run helm-template
359
editor:
360
name: editor
361
runs-on: ubuntu-24.04
362
permissions:
363
contents: read
364
steps:
365
- name: Checkout
366
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
367
with:
368
persist-credentials: false
370
- name: Setup Mise
371
uses: jdx/mise-action@7e36c90d9ab29c415a2384db3006f3ec8a8cc654 # v4.2.4
372
with:
373
experimental: true
374
install: false
376
- name: Install the pinned Go toolchain
377
run: |
378
set -euo pipefail
379
mise install go --locked
380
echo "$(mise where go)/bin" >> "$GITHUB_PATH"
382
- name: Cache cross-compilation
383
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
384
with:
385
path: |
386
~/.cache/go-build
387
~/go/pkg/mod
388
key: ${{ runner.os }}-go-cross-${{ hashFiles('go.sum') }}
389
restore-keys: ${{ runner.os }}-go-cross-
391
- name: Install locked editor tools
392
run: mise --cd editors/vscode install --locked
394
- name: Install editor packaging tools
395
run: mise --cd editors/vscode exec -- npm ci
397
- name: Test extension protocol
398
run: mise run extension-test
400
- name: Build the archives the extension bundles
401
run: |
402
set -euo pipefail
403
test "$(go env GOVERSION)" = "go$(mise config get tools.go)"
404
version=0.0.0-ci
405
mkdir -p archives
406
cp LICENSE README.md archives/
407
for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64; do
408
os=${target%/*}
409
arch=${target#*/}
410
binary=koment
411
[ "$os" = windows ] && binary=koment.exe
412
CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -trimpath -o "archives/${binary}" ./cmd/koment
413
name="koment_${version}_${os}_${arch}"
414
if [ "$os" = windows ]; then
415
(cd archives && zip -q "${name}.zip" "$binary" LICENSE README.md && rm "$binary")
416
else
417
(cd archives && tar -czf "${name}.tar.gz" "$binary" LICENSE README.md && rm "$binary")
418
fi
419
done
420
rm archives/LICENSE archives/README.md
421
(cd archives && sha256sum ./*.tar.gz ./*.zip | sed 's| \./| |' > "koment_${version}_checksums.txt")
423
- name: Validate every VSIX
424
run: |
425
set -euo pipefail
426
version=0.0.0-ci
427
manifest=editors/vscode/package.json
428
cp "$manifest" "$RUNNER_TEMP/package.json"
429
trap 'cp "$RUNNER_TEMP/package.json" "$manifest"' EXIT
430
node -e "
431
const p = require('./editors/vscode/package.json');
432
p.version = '$version';
433
require('fs').writeFileSync('editors/vscode/package.json', JSON.stringify(p, null, 2) + '\n');
434
"
435
./editors/vscode/package-vsix.sh "$version" archives dist >/dev/null
437
for pair in linux-x64:koment linux-arm64:koment darwin-x64:koment darwin-arm64:koment \
438
win32-x64:koment.exe win32-arm64:koment.exe; do
439
target=${pair%%:*}
440
expected=${pair##*:}
441
package="dist/koment-vscode_${version}_${target}.vsix"
442
carried=$(unzip -l "$package" | awk '/extension\/bin\//{print $NF}' | sed 's|extension/bin/||')
443
test "$carried" = "$expected" || {
444
echo "::error::$package carries '$carried', expected '$expected'"
445
exit 1
446
}
447
unzip -p "$package" extension.vsixmanifest | grep -q "TargetPlatform=\"${target}\"" || {
448
echo "::error::$package is not declared for $target"
449
exit 1
450
}
451
echo "$package: $expected for $target"
452
done
454
universal="dist/koment-vscode_${version}.vsix"
455
if unzip -l "$universal" | grep -q 'extension/bin/'; then
456
echo "::error::the universal package must carry no binary"
457
exit 1
458
fi
459
echo "$universal: no binary, falls back to PATH"
461
ci:
462
if: ${{ always() }}
463
name: ci
464
needs:
465
- container
466
- editor
467
- helm
468
- lint
469
- quality
470
- setup-action
471
- test
472
runs-on: ubuntu-24.04
473
permissions: {}
474
steps:
475
- name: Reject failed, cancelled, or skipped jobs
476
if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }}
477
run: exit 1
479
- name: Accept successful jobs
480
if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped')) }}
481
run: echo "All CI jobs passed"