chore(ci): add shared-lib scaffolding and functional coverage gate
Bring the auth and logging libs up to the otelsetup/authz_client standard: golangci-lint + .editorconfig + .testcoverage.yml + cliff.toml + renovate.json + CHANGELOG + CLAUDE.md + pre-commit and Release workflows. Replace the minimal test-only CI with a cache-based coverage-regression gate (PR test job restores main's baseline from the Actions cache; a non-gating post-merge coverage-baseline job records it) mirroring the services (ADR-0010 carve-out). Job names test/vulnerabilities preserved to match branch-protection contexts.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
@@ -20,7 +20,79 @@ jobs:
|
|||||||
go install mvdan.cc/gofumpt@latest
|
go install mvdan.cc/gofumpt@latest
|
||||||
test -z "$(gofumpt -l .)"
|
test -z "$(gofumpt -l .)"
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -race ./...
|
run: go test -race -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
- name: Filter test files from coverage
|
||||||
|
run: |
|
||||||
|
grep -v -E '_test\.go:' coverage.txt > coverage.filtered.txt || true
|
||||||
|
mv coverage.filtered.txt coverage.txt
|
||||||
|
|
||||||
|
- name: Check coverage
|
||||||
|
id: coverage
|
||||||
|
run: |
|
||||||
|
go install github.com/vladopajic/go-test-coverage/v2@latest
|
||||||
|
go-test-coverage --config ./.testcoverage.yml --github-action-output
|
||||||
|
- name: Restore baseline coverage
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: coverage-baseline.txt
|
||||||
|
key: coverage-baseline-${{ gitea.run_id }}
|
||||||
|
restore-keys: |
|
||||||
|
coverage-baseline-
|
||||||
|
- name: Compare coverage
|
||||||
|
run: |
|
||||||
|
CURRENT="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
|
if [ -f coverage-baseline.txt ]; then
|
||||||
|
BASE=$(cat coverage-baseline.txt)
|
||||||
|
echo "Base coverage: ${BASE}%"
|
||||||
|
echo "Current coverage: ${CURRENT}%"
|
||||||
|
if [ "$(echo "$CURRENT < $BASE" | bc -l)" -eq 1 ]; then
|
||||||
|
echo "::error::Coverage decreased from ${BASE}% to ${CURRENT}%"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Coverage maintained or improved: ${BASE}% -> ${CURRENT}%"
|
||||||
|
else
|
||||||
|
echo "No baseline coverage found yet, skipping comparison"
|
||||||
|
echo "Current coverage: ${CURRENT}%"
|
||||||
|
fi
|
||||||
|
- name: Post coverage comment
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ gitea.server_url }}
|
||||||
|
run: |
|
||||||
|
COVERAGE="${{ steps.coverage.outputs.total-coverage }}"
|
||||||
|
curl -X POST "${GITEA_URL}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"body\": \"## Coverage Report\n\nTotal coverage: **${COVERAGE}%**\"}"
|
||||||
|
|
||||||
|
coverage-baseline:
|
||||||
|
# Records main's coverage into the Actions cache for the next PR's
|
||||||
|
# regression gate to read. Post-merge only, not a required check, blocks
|
||||||
|
# nothing (cf. ADR-0010).
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: 'stable'
|
||||||
|
- name: Compute coverage
|
||||||
|
id: coverage
|
||||||
|
run: |
|
||||||
|
go install github.com/vladopajic/go-test-coverage/v2@latest
|
||||||
|
go test -coverprofile=coverage.txt ./...
|
||||||
|
grep -v -E '_test\.go:' coverage.txt > coverage.filtered.txt || true
|
||||||
|
mv coverage.filtered.txt coverage.txt
|
||||||
|
go-test-coverage --config ./.testcoverage.yml --github-action-output
|
||||||
|
- name: Write baseline file
|
||||||
|
run: echo "${{ steps.coverage.outputs.total-coverage }}" > coverage-baseline.txt
|
||||||
|
- name: Save baseline to cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: coverage-baseline.txt
|
||||||
|
key: coverage-baseline-${{ gitea.run_id }}
|
||||||
|
|
||||||
vulnerabilities:
|
vulnerabilities:
|
||||||
if: gitea.event_name == 'pull_request'
|
if: gitea.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
name: pre-commit
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-commit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
SKIP: no-commit-to-branch
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
- uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: stable
|
||||||
|
- uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.14'
|
||||||
|
- name: Install goimports
|
||||||
|
run: go install golang.org/x/tools/cmd/goimports@latest
|
||||||
|
- uses: pre-commit/action@v3.0.1
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
uses: unboundsoftware/shared-workflows/.gitea/workflows/Release.yml@main
|
||||||
@@ -2,3 +2,4 @@
|
|||||||
.claude
|
.claude
|
||||||
/release
|
/release
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
coverage-baseline.txt
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
allow-parallel-runners: true
|
||||||
|
linters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
presets:
|
||||||
|
- comments
|
||||||
|
- common-false-positives
|
||||||
|
- legacy
|
||||||
|
- std-error-handling
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
args:
|
||||||
|
- --allow-multiple-documents
|
||||||
|
- id: check-added-large-files
|
||||||
|
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
|
||||||
|
rev: v9.25.0
|
||||||
|
hooks:
|
||||||
|
- id: commitlint
|
||||||
|
stages: [ commit-msg ]
|
||||||
|
additional_dependencies: [ '@commitlint/config-conventional' ]
|
||||||
|
- repo: https://github.com/dnephin/pre-commit-golang
|
||||||
|
rev: v0.5.1
|
||||||
|
hooks:
|
||||||
|
- id: go-mod-tidy
|
||||||
|
- id: go-imports
|
||||||
|
args:
|
||||||
|
- -local
|
||||||
|
- gitea.unbound.se/shiny/logging
|
||||||
|
- repo: https://github.com/lietu/go-pre-commit
|
||||||
|
rev: v1.0.0
|
||||||
|
hooks:
|
||||||
|
- id: go-test
|
||||||
|
- id: gofumpt
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v2.12.2
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint-full
|
||||||
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
|
rev: v8.30.1
|
||||||
|
hooks:
|
||||||
|
- id: gitleaks
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Coverage configuration for go-test-coverage
|
||||||
|
# https://github.com/vladopajic/go-test-coverage
|
||||||
|
|
||||||
|
profile: coverage.txt
|
||||||
|
|
||||||
|
threshold:
|
||||||
|
file: 0
|
||||||
|
package: 0
|
||||||
|
total: 0
|
||||||
|
|
||||||
|
exclude:
|
||||||
|
paths:
|
||||||
|
- _test\.go$
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-06-15
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Initial version: `SetupLogger` + context-logger helpers, the `MockLogger` test helper, and a `middleware` request-logger sub-package, extracted from the per-service copies.
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# logging
|
||||||
|
|
||||||
|
Shared Go library with logging primitives for all Shiny backend services.
|
||||||
|
|
||||||
|
## Shared Documentation
|
||||||
|
|
||||||
|
@../docs/claude/architecture.md
|
||||||
|
@../docs/claude/go-services.md
|
||||||
|
@../docs/claude/conventions.md
|
||||||
|
|
||||||
|
## Library Information
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Single home for the `slog` setup, context-logger helpers, request-logging
|
||||||
|
middleware and the `MockLogger` test helper that were previously copied (with
|
||||||
|
drift) into every backend service.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"gitea.unbound.se/shiny/logging"
|
||||||
|
logmw "gitea.unbound.se/shiny/logging/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure the slog default logger (format: text | json | otel).
|
||||||
|
logger := logging.SetupLogger(cli.LogLevel, cli.LogFormat, serviceName, buildVersion)
|
||||||
|
|
||||||
|
// Carry a logger on the request context.
|
||||||
|
ctx = logging.ContextWithLogger(ctx, logger)
|
||||||
|
logger = logging.LoggerFromContext(ctx)
|
||||||
|
|
||||||
|
// Debug-log request/response bodies.
|
||||||
|
handler = logmw.RequestLogger(logger)(handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
In tests:
|
||||||
|
|
||||||
|
```go
|
||||||
|
m := logging.NewMockLogger()
|
||||||
|
// ... exercise code with m.Logger() ...
|
||||||
|
m.Check(t, []string{"level=INFO msg=\"...\""})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exported API
|
||||||
|
|
||||||
|
- `SetupLogger(logLevel, logFormat, serviceName, buildVersion string) *slog.Logger`
|
||||||
|
- `ContextWithLogger(ctx, *slog.Logger) context.Context` / `LoggerFromContext(ctx) *slog.Logger`
|
||||||
|
- `Logger` interface; `NewMockLogger() *MockLogger` (+ `MockLogger.Logger()`, `MockLogger.Check(t, want)`).
|
||||||
|
- `logging/middleware.RequestLogger(logger) func(http.Handler) http.Handler`.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- `MockLogger` currently lives in the main package, so `testify` is a non-test
|
||||||
|
dependency of the module. Moving it to a `logging/logtest` sub-package is a
|
||||||
|
tracked low-priority follow-up — it's a breaking import change for the ~13
|
||||||
|
services that reference `logging.NewMockLogger`, so it is deferred until a
|
||||||
|
coordinated bump (Ambix 019ecabc).
|
||||||
|
|
||||||
|
### Conventions
|
||||||
|
|
||||||
|
Standard Shiny library scaffolding: `gofumpt`/`goimports -local`, golangci-lint,
|
||||||
|
gitleaks and conventional-commit checks via pre-commit; coverage-regression gate
|
||||||
|
in CI (`.testcoverage.yml`); releases auto-tagged from conventional commits by
|
||||||
|
the shared Release workflow. Bump the consuming services' `go.mod` after a
|
||||||
|
release.
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
# git-cliff ~ default configuration file
|
||||||
|
# https://git-cliff.org/docs/configuration
|
||||||
|
#
|
||||||
|
# Lines starting with "#" are comments.
|
||||||
|
# Configuration options are organized into tables and keys.
|
||||||
|
# See documentation for more information on available options.
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# template for the changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
All notable changes to this project will be documented in this file.\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||||
|
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# template for the changelog footer
|
||||||
|
footer = """
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing s
|
||||||
|
trim = true
|
||||||
|
# postprocessors
|
||||||
|
postprocessors = [
|
||||||
|
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
|
||||||
|
]
|
||||||
|
# render body even when there are no releases to process
|
||||||
|
# render_always = true
|
||||||
|
# output file path
|
||||||
|
# output = "test.md"
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = true
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
# Replace issue numbers
|
||||||
|
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
||||||
|
# Check spelling of the commit with https://github.com/crate-ci/typos
|
||||||
|
# If the spelling is incorrect, it will be automatically fixed.
|
||||||
|
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||||
|
]
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||||
|
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||||
|
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||||
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
|
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||||
|
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||||
|
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||||
|
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||||
|
]
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = false
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user