Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 106 additions & 86 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ concurrency:
cancel-in-progress: true

jobs:
# ---------------------------------------------------------------------------------------------
# Go server — mirrors gate-build.sh "go-build" / "go-vet".
# ---------------------------------------------------------------------------------------------
go-build:
name: Go build (server)
runs-on: ubuntu-latest
Expand All @@ -26,10 +23,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
# Pinned from the module itself (`go 1.25.0`) so CI can never drift from go.mod.
go-version-file: server/go.mod
# setup-go's built-in cache covers both the module cache (GOMODCACHE) and the compiler
# build cache (GOCACHE), keyed on go.sum.
cache: true
cache-dependency-path: server/go.sum

Expand All @@ -41,16 +35,12 @@ jobs:
working-directory: server
run: go vet ./cmd/... ./internal/... ./tests/...

- name: go build (CGO_ENABLED=0) — NFR-005
- name: go build (CGO_ENABLED=0)
working-directory: server

env:
CGO_ENABLED: '0'
run: go build -o /dev/null ./cmd/... ./internal/... ./tests/...

# ---------------------------------------------------------------------------------------------
# Web UI — mirrors gate-build.sh "web-install" / "web-build" (+ gate-lint.sh's cheap checks).
# ---------------------------------------------------------------------------------------------
web-build:
name: Web build (server/web)
runs-on: ubuntu-latest
Expand All @@ -64,8 +54,6 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v4
with:
# package.json engines is a floor (">=20.19.0"), not a pin. 24 is the current LTS and the
# major developers run locally; bump deliberately, not implicitly.
node-version: '24'
cache: npm
cache-dependency-path: server/web/package-lock.json
Expand All @@ -74,25 +62,23 @@ jobs:
run: npm ci

- name: Type-check
# vue-tsc failures are compile errors, not style opinions — they fail the job.
run: npm run typecheck

- name: Build
run: npm run build

- name: Lint (advisory)
# eslint + prettier. Reported, not enforced: no gates are enforced until D1.14.
- name: Lint
continue-on-error: true
run: npm run lint


openapi-drift:
name: OpenAPI drift (P-4/NFR-032/NFR-033)
openapi:
name: OpenAPI lint and contract tests
runs-on: ubuntu-latest

timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
Expand All @@ -104,37 +90,39 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v4
with:
# For `npx @redocly/cli lint`, invoked by gate-contract-test.sh's openapi-lint leg.
node-version: '24'
cache: npm
cache-dependency-path: server/web/package-lock.json

- name: Contract test gate (openapi-lint + openapi-drift + contract-tests)
run:../scripts/gate-contract-test.sh
- name: Lint openapi.yaml
run: npx --yes @redocly/cli@latest lint server/api/openapi.yaml

- name: NFR-033 additive-only check (pull_request only)
- name: Contract tests
working-directory: server
run: go test ./tests/contract/...

- name: Check for breaking API changes
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
git fetch --depth=1 origin "${{ github.event.pull_request.base.sha }}"
git show "${{ github.event.pull_request.base.sha }}:server/api/openapi.yaml" > /tmp/base-openapi.yaml
../scripts/check-additive-api.sh /tmp/base-openapi.yaml server/api/openapi.yaml

- name: NFR-033 additive-only check skipped (no base ref on this event)

if: github.event_name != 'pull_request'
run: echo "::notice::NFR-033 additive-only check skipped — no PR base ref on a '${{ github.event_name }}' event. The openapi-drift step above still ran."
git show "${{ github.event.pull_request.base.sha }}:server/api/openapi.yaml" > "$RUNNER_TEMP/base-openapi.yaml" || {
echo "::notice::openapi.yaml does not exist on the base branch, nothing to compare."
exit 0
}
cp server/api/openapi.yaml "$RUNNER_TEMP/openapi.yaml"
docker run --rm -v "$RUNNER_TEMP:/specs:ro" tufin/oasdiff \
breaking /specs/base-openapi.yaml /specs/openapi.yaml --fail-on ERR

compose-smoke:
name: Compose smoke (NFR-026)
name: Compose smoke test
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image (tagged to match compose.yaml's pin — see job comment above)

# Tagged to match the image pinned in compose.yaml, so compose uses the local build.
- name: Build image
uses: docker/build-push-action@v6
with:
context: .
Expand All @@ -146,22 +134,40 @@ jobs:
cache-from: type=gha,scope=compose-smoke
cache-to: type=gha,mode=max,scope=compose-smoke

- name: Run compose-smoke gate (NFR-026)
run: ./scripts/gate-compose-smoke.sh
- name: Start stack
run: docker compose up -d --pull never

- name: Upload gate logs
- name: Wait for healthy container
run: |
for _ in $(seq 1 60); do
status=$(docker inspect --format '{{.State.Health.Status}}' hho)
if [ "$status" = "healthy" ]; then
echo "container is healthy"
exit 0
fi
sleep 2
done
echo "::error::container did not become healthy in time"
docker compose logs
exit 1

- name: Smoke request
run: curl -fsS "http://localhost:7745/api/v1/status"

- name: Logs
if: failure()
run: docker compose logs

- name: Tear down
if: always()
uses: actions/upload-artifact@v4
with:
name: compose-smoke-gate-logs
path: .tmp/current/gates/
if-no-files-found: ignore
retention-days: 7
run: docker compose down -v

go-coverage:
name: Go coverage >= 70% (NFR-030)
go-test:
name: Go tests and coverage
runs-on: ubuntu-latest
timeout-minutes: 20
env:
COVERAGE_THRESHOLD: '65'
steps:
- uses: actions/checkout@v4

Expand All @@ -172,21 +178,33 @@ jobs:
cache: true
cache-dependency-path: server/go.sum

- name: Go coverage gate (race + 70% threshold — NFR-030)
run:../scripts/gate-go-coverage.sh
# The Android stocktake replay tests need golden files from the android-app repo.
- name: go test (race + coverage)
working-directory: server
run: go test -race -skip 'TestSyncPushAndroidStocktake' -covermode=atomic -coverprofile=coverage.out ./cmd/hho/... ./internal/... ./tests/...

- name: Check coverage threshold
working-directory: server
run: |
total=$(go tool cover -func=coverage.out | awk '/^total:/ { sub("%", "", $3); print $3 }')
echo "Total coverage: ${total}%"
echo "Total coverage: ${total}%" >> "$GITHUB_STEP_SUMMARY"
if awk -v t="$total" -v min="$COVERAGE_THRESHOLD" 'BEGIN { exit !(t < min) }'; then
echo "::error::coverage ${total}% is below ${COVERAGE_THRESHOLD}%"
exit 1
fi

- name: Upload coverage profile
if: always()
uses: actions/upload-artifact@v4
with:
name: go-coverage-profile
path: .tmp/current/gates/go-coverage.out
path: server/coverage.out
if-no-files-found: ignore
retention-days: 7


go-govulncheck:
name: govulncheck (NFR-015)
govulncheck:
name: govulncheck
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
Expand All @@ -199,22 +217,16 @@ jobs:
cache: true
cache-dependency-path: server/go.sum

- name: govulncheck gate (NFR-015)
run: ./scripts/gate-govulncheck.sh

- name: Upload govulncheck logs
if: always()
uses: actions/upload-artifact@v4
with:
name: govulncheck-logs
path: .tmp/current/gates/govulncheck*.log
if-no-files-found: ignore
retention-days: 7
- name: Run govulncheck
working-directory: server
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...

image-size:
name: Image size < 40 MB (NFR-003)
name: Image size < 40 MB
runs-on: ubuntu-latest
timeout-minutes: 15
env:
MAX_IMAGE_BYTES: '41943040'
steps:
- uses: actions/checkout@v4

Expand All @@ -229,26 +241,23 @@ jobs:
platforms: linux/amd64
load: true
push: false
tags: hho:gate
tags: hho:ci
cache-from: type=gha,scope=image-size
cache-to: type=gha,mode=max,scope=image-size

- name: Image-size gate (NFR-003)
run: ./scripts/gate-image-size.sh

- name: Upload gate logs
if: always()
uses: actions/upload-artifact@v4
with:
name: image-size-gate-logs
path: .tmp/current/gates/
if-no-files-found: ignore
retention-days: 7
- name: Check image size
run: |
size=$(docker image inspect hho:ci --format '{{.Size}}')
echo "Image size: $((size / 1024 / 1024)) MB ($size bytes)"
echo "Image size: $((size / 1024 / 1024)) MB" >> "$GITHUB_STEP_SUMMARY"
if [ "$size" -gt "$MAX_IMAGE_BYTES" ]; then
echo "::error::image is larger than $((MAX_IMAGE_BYTES / 1024 / 1024)) MB"
exit 1
fi

latency:
name: p95 API latency < 100ms (NFR-002)
name: API latency benchmark (p95 < 100ms)
runs-on: ubuntu-latest

timeout-minutes: 15
steps:
- uses: actions/checkout@v4
Expand All @@ -260,15 +269,26 @@ jobs:
cache: true
cache-dependency-path: server/go.sum

- name: p95 API latency gate (NFR-002/SC-003)
run:../scripts/gate-latency.sh

- name: Upload gate logs and JSON result
- name: Run benchmark
working-directory: server
run: go run ./cmd/bench-api-latency -out latency.json

- name: Check p95 budget
working-directory: server
run: |
worst=$(jq '[.runs[] | .list.p95_ms, .search.p95_ms, .detail.p95_ms] | max' latency.json)
echo "Worst p95: ${worst} ms"
echo "Worst p95: ${worst} ms" >> "$GITHUB_STEP_SUMMARY"
if jq -e --argjson w "$worst" '$w >= 100' <<< 'null' > /dev/null; then
echo "::error::p95 latency ${worst} ms is over the 100 ms budget"
exit 1
fi

- name: Upload result
if: always()
uses: actions/upload-artifact@v4
with:
name: latency-gate-logs
path: .tmp/current/gates/
name: latency-result
path: server/latency.json
if-no-files-found: ignore
retention-days: 7
retention-days: 7
2 changes: 1 addition & 1 deletion .github/workflows/release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
if: always()
run: |
echo "### Published raw binaries for tag \`${{ github.ref_name }}\`" >> "$GITHUB_STEP_SUMMARY"
echo "linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 -- see the release's assets and \`checksums.txt\`." >> "$GITHUB_STEP_SUMMARY"
echo "linux/amd64, linux/arm64 -- see the release's assets and \`checksums.txt\`." >> "$GITHUB_STEP_SUMMARY"
14 changes: 0 additions & 14 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ before:
builds:
- id: hho
binary: hho
# `go build ... ./cmd/hho`.
dir: server
main: ./cmd/hho
env:
Expand All @@ -21,22 +20,14 @@ builds:
- -s -w -X main.version={{ .Version }}
goos:
- linux
# - darwin
# - windows
goarch:
- amd64
- arm64
ignore:
- goos: windows
goarch: arm64

archives:
- id: hho
ids: [hho]
formats: [tar.gz]
format_overrides:
- goos: windows
formats: [zip]
name_template: >-
hho_{{ .Version }}_{{ .Os }}_{{ .Arch }}
files:
Expand All @@ -50,9 +41,4 @@ changelog:
disable: true

release:
github:
owner: ankek
name: Household-Objects-Dev
name_template: "v{{ .Version }}"
footer: |
See [CHANGELOG.md](https://github.com/ankek/Household-Objects-Dev/blob/main/CHANGELOG.md) for the full release notes.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ RUN npm ci
COPY server/web/ ./
RUN npm run build

FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS build
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS build
WORKDIR /src/server
COPY server/go.mod server/go.sum ./
RUN go mod download
Expand Down
3 changes: 2 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ services:
- '${HHO_PORT:-7745}:7745'

volumes:
- hho-data:/data
# To use a host directory instead:
# mkdir -p ./data && sudo chown -R 65532:65532 ./data
#
# - ./data:/data

environment:
Expand Down
Loading
Loading